Compare commits
360 Commits
disable-ya
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15a7507a09 | ||
|
|
cdf721a273 | ||
|
|
cafe7ea35c | ||
|
|
e18ca373eb | ||
|
|
e54f927149 | ||
|
|
6735902c1a | ||
|
|
dc3ffac4a2 | ||
|
|
fe885d5788 | ||
|
|
adfe7242f7 | ||
|
|
dfcfef38af | ||
|
|
fbc116ef90 | ||
|
|
6b5e18fb1d | ||
|
|
d39f7bc72f | ||
|
|
e68c1c824a | ||
|
|
076c8ec51e | ||
|
|
f5b57e8ba7 | ||
|
|
0786c1e57a | ||
|
|
ec2a99341c | ||
|
|
6e7e8de343 | ||
|
|
a444a0b572 | ||
|
|
6f8558a6b9 | ||
|
|
22203f8aeb | ||
|
|
f28715d370 | ||
|
|
fcf012c602 | ||
|
|
dee1dc41aa | ||
|
|
655de32990 | ||
|
|
99db6a1910 | ||
|
|
d5a7b383fa | ||
|
|
34c91555ae | ||
|
|
bd77f2e86d | ||
|
|
de7282d1cd | ||
|
|
7f5b16a6ad | ||
|
|
e3f81c7368 | ||
|
|
b36c121a53 | ||
|
|
b3992e62ed | ||
|
|
1232b55211 | ||
|
|
5f33cf0b0a | ||
|
|
c3afdb760c | ||
|
|
40f5533990 | ||
|
|
eec97e387a | ||
|
|
eea90c205a | ||
|
|
7592813e15 | ||
|
|
f0204f3e62 | ||
|
|
01434ad4b6 | ||
|
|
c26003af21 | ||
|
|
07a05e1edf | ||
|
|
2402730083 | ||
|
|
28ae61f34d | ||
|
|
dcb6dbbc86 | ||
|
|
99b72f60ad | ||
|
|
19914e9ef6 | ||
|
|
a05d2d7ee2 | ||
|
|
19b19ad8c2 | ||
|
|
8f47470853 | ||
|
|
75024a1778 | ||
|
|
db304735bf | ||
|
|
bb94f91f86 | ||
|
|
cdf48e806d | ||
|
|
b946b8679d | ||
|
|
3294b5777f | ||
|
|
f095346f8f | ||
|
|
d70c807a76 | ||
|
|
630ad9fd49 | ||
|
|
8bbde181db | ||
|
|
13fbf00a97 | ||
|
|
0ef5dca3c8 | ||
|
|
771fdcbb9f | ||
|
|
758db36ec7 | ||
|
|
bbb3392dbe | ||
|
|
cb5c5432b3 | ||
|
|
7c05f56fe8 | ||
|
|
d2f640272f | ||
|
|
2f1bbe051c | ||
|
|
a547dfff37 | ||
|
|
735a00d741 | ||
|
|
a6a8a37ae1 | ||
|
|
82ce9367c3 | ||
|
|
e081d5936c | ||
|
|
c3a1e04692 | ||
|
|
817a0a6764 | ||
|
|
c45287c72d | ||
|
|
2a890822e3 | ||
|
|
2e7df27180 | ||
|
|
8e03c9c1fc | ||
|
|
b1f7c9641f | ||
|
|
e7ed8bb682 | ||
|
|
dcc26c1b24 | ||
|
|
8d6406f561 | ||
|
|
2543425e04 | ||
|
|
cf7a092053 | ||
|
|
86e4ecfa20 | ||
|
|
658ad9f57e | ||
|
|
b71333921b | ||
|
|
9ff094b62e | ||
|
|
b2aa476abb | ||
|
|
496d41cdce | ||
|
|
674e2685be | ||
|
|
fcd56d6732 | ||
|
|
53d0499254 | ||
|
|
27e90864ac | ||
|
|
9c8e1855a5 | ||
|
|
11803e3d04 | ||
|
|
c47922602f | ||
|
|
60a437e045 | ||
|
|
f24f98ce40 | ||
|
|
2fed2edd5e | ||
|
|
92c9fda9e6 | ||
|
|
d0c8eb2f1b | ||
|
|
90c812ed16 | ||
|
|
1a2038775c | ||
|
|
65b7ddb3e8 | ||
|
|
f6f45c43a9 | ||
|
|
0f753038c4 | ||
|
|
fee4c262d2 | ||
|
|
a2d04ee7b4 | ||
|
|
194b889873 | ||
|
|
1e3b089eb6 | ||
|
|
07ce066d68 | ||
|
|
b653660a5c | ||
|
|
b04f7e7411 | ||
|
|
9ef8df569e | ||
|
|
d243ba36ce | ||
|
|
aee0025ca3 | ||
|
|
cb2e770584 | ||
|
|
5e3e11bbfa | ||
|
|
3bc27b9b64 | ||
|
|
127de5bb3d | ||
|
|
0aae54dfd9 | ||
|
|
066456ecdf | ||
|
|
e715531dd3 | ||
|
|
ba83509ff4 | ||
|
|
a23b3c7c25 | ||
|
|
ee88da4511 | ||
|
|
46ccfa6e8d | ||
|
|
5922d0181e | ||
|
|
030104a30c | ||
|
|
ff99131776 | ||
|
|
a7001f52ab | ||
|
|
708fe31908 | ||
|
|
00c2089e81 | ||
|
|
696aaa616b | ||
|
|
e4b8bbe6e8 | ||
|
|
aa6baf15aa | ||
|
|
ea52f76314 | ||
|
|
b44aa94853 | ||
|
|
c6facd27ed | ||
|
|
c79bd31234 | ||
|
|
322ada898f | ||
|
|
d63ca75422 | ||
|
|
59f3d8a993 | ||
|
|
c270634565 | ||
|
|
b1703467f1 | ||
|
|
b076808fd2 | ||
|
|
945ac23910 | ||
|
|
3021cd8002 | ||
|
|
a8c261ae7c | ||
|
|
d4e7af910c | ||
|
|
20e3265f3b | ||
|
|
88b21e587c | ||
|
|
c18db97254 | ||
|
|
d4e60dae9a | ||
|
|
8456616793 | ||
|
|
9c5ef8f3f6 | ||
|
|
5288abfb03 | ||
|
|
6dbad32d65 | ||
|
|
1df259f8c9 | ||
|
|
deb72a4c91 | ||
|
|
a47ed31047 | ||
|
|
5b395774c0 | ||
|
|
afeb63d287 | ||
|
|
725d8983fa | ||
|
|
b761310823 | ||
|
|
578836f9ae | ||
|
|
41a3679d83 | ||
|
|
614eda43ff | ||
|
|
b193913f46 | ||
|
|
eb5bfa4541 | ||
|
|
31f89617d8 | ||
|
|
ffd7160980 | ||
|
|
6c5bd4f9a8 | ||
|
|
763e2ddc49 | ||
|
|
5d9796afb2 | ||
|
|
bd17c48ef9 | ||
|
|
03045425b7 | ||
|
|
4f76bdfcb7 | ||
|
|
3d5cb624ba | ||
|
|
1f1653e039 | ||
|
|
d5f8b08d69 | ||
|
|
2dd630bc58 | ||
|
|
2b93a2211f | ||
|
|
c53bb2fcd6 | ||
|
|
b1cea4a3d3 | ||
|
|
74d5f99ba3 | ||
|
|
45221070cc | ||
|
|
3473b8a652 | ||
|
|
a4e5c3244f | ||
|
|
ff57ef2c9b | ||
|
|
a217b633b7 | ||
|
|
be4ba1495c | ||
|
|
0142a4a9de | ||
|
|
7c1d6ab114 | ||
|
|
e2be688389 | ||
|
|
2f0db28aa4 | ||
|
|
d7b60a2cb6 | ||
|
|
478dae0ab3 | ||
|
|
a8741495c4 | ||
|
|
540042dfe3 | ||
|
|
1d1deaab2a | ||
|
|
5bc69ea668 | ||
|
|
fdb2563abf | ||
|
|
c4eec632b9 | ||
|
|
5b1891a1ae | ||
|
|
e3c0883d32 | ||
|
|
1cae543e8f | ||
|
|
bc09d3c5f2 | ||
|
|
58df263159 | ||
|
|
a3127a146d | ||
|
|
a706fce678 | ||
|
|
57c5d1c8dd | ||
|
|
c589530e22 | ||
|
|
9717dc64d0 | ||
|
|
5399d9761c | ||
|
|
27d92ede7e | ||
|
|
ec855cbf59 | ||
|
|
6903d15559 | ||
|
|
055b739b88 | ||
|
|
d82bada742 | ||
|
|
28e5c3bb51 | ||
|
|
ccf5c09ad3 | ||
|
|
c0b1fbe0a9 | ||
|
|
d9149bfed9 | ||
|
|
0d283cc48e | ||
|
|
06417e2b92 | ||
|
|
45fbb3b053 | ||
|
|
225fe58d5c | ||
|
|
048700da2f | ||
|
|
bdad4f78f3 | ||
|
|
b15d234ccb | ||
|
|
05a1c170c2 | ||
|
|
ea33d7fba6 | ||
|
|
1d3ca80bf7 | ||
|
|
9afaa23e78 | ||
|
|
475e6833ff | ||
|
|
b846f88e16 | ||
|
|
5722b1bbc5 | ||
|
|
570f2ef482 | ||
|
|
e571994b5c | ||
|
|
3411d06f9e | ||
|
|
d5f0e37260 | ||
|
|
3c88310f37 | ||
|
|
58f0a80ae9 | ||
|
|
a40b071640 | ||
|
|
0e6180a5af | ||
|
|
fc1ba93cdc | ||
|
|
0e4ee62dfc | ||
|
|
e711f9d492 | ||
|
|
5a38246ee8 | ||
|
|
0ef00be494 | ||
|
|
961acaf202 | ||
|
|
e05ac2ec04 | ||
|
|
b17c544f1d | ||
|
|
89611bf32c | ||
|
|
18c79e4e45 | ||
|
|
21a6ecbfb4 | ||
|
|
fee38e57f0 | ||
|
|
543db6d24c | ||
|
|
6b1e1899fd | ||
|
|
3a84990780 | ||
|
|
d6f62f5fa4 | ||
|
|
fab1e799a6 | ||
|
|
4835c3b7b4 | ||
|
|
298fc7ce4c | ||
|
|
e71d6fa344 | ||
|
|
32edf53ea9 | ||
|
|
d9ea631d59 | ||
|
|
e9af9c649f | ||
|
|
f6652caef4 | ||
|
|
75bbf73737 | ||
|
|
9439a2e944 | ||
|
|
05c9ebf2ce | ||
|
|
2b93d19d2c | ||
|
|
d931e2f30d | ||
|
|
ba0b9e8ea5 | ||
|
|
4fcab304e3 | ||
|
|
6142c7b003 | ||
|
|
63a244fe1a | ||
|
|
02deb0b238 | ||
|
|
96c8eeba49 | ||
|
|
cea0cbdb73 | ||
|
|
46af7467e0 | ||
|
|
06a8379dce | ||
|
|
7b343c9567 | ||
|
|
f98d8157d6 | ||
|
|
f4f1a86da6 | ||
|
|
fa529c1883 | ||
|
|
896e15bd4a | ||
|
|
bb64905b21 | ||
|
|
73fc8d34d9 | ||
|
|
8124d44ee1 | ||
|
|
3b39562954 | ||
|
|
a896081808 | ||
|
|
df4b4f1620 | ||
|
|
9c164aa16c | ||
|
|
8bc0eaa1bb | ||
|
|
eed704d4bd | ||
|
|
d6c0b93c85 | ||
|
|
ef4a583f54 | ||
|
|
f429019f34 | ||
|
|
2ef9cceccd | ||
|
|
d6f8ac97e8 | ||
|
|
19ef4e5c40 | ||
|
|
245c03664a | ||
|
|
875cd30150 | ||
|
|
79505180a5 | ||
|
|
519b00f25b | ||
|
|
cec3e82b21 | ||
|
|
05bed6f3d8 | ||
|
|
34514f00da | ||
|
|
c2fafce995 | ||
|
|
4e60a6f163 | ||
|
|
b6f09b9a2b | ||
|
|
66fdd3ae65 | ||
|
|
4ad54b279d | ||
|
|
97ba08113d | ||
|
|
ba9eabccbf | ||
|
|
28b04ec24e | ||
|
|
7d9b1e6d1e | ||
|
|
e65fedd672 | ||
|
|
39c70649ca | ||
|
|
df64716b34 | ||
|
|
99a219036f | ||
|
|
f091e7050c | ||
|
|
890452f54a | ||
|
|
7b0da9bb4e | ||
|
|
b4d597af93 | ||
|
|
efea53e7a3 | ||
|
|
c155e0d58b | ||
|
|
4299e33389 | ||
|
|
f597589695 | ||
|
|
de86ad56e5 | ||
|
|
e4e7e679b3 | ||
|
|
db13dddcf9 | ||
|
|
927c7d747f | ||
|
|
8e212fca59 | ||
|
|
31d2885d95 | ||
|
|
85fb9218a7 | ||
|
|
ed6ceda71d | ||
|
|
9fdc8246f2 | ||
|
|
f2f07404b5 | ||
|
|
71e6e50846 | ||
|
|
4633b97c55 | ||
|
|
0a64bcae63 | ||
|
|
8fa91b4b81 | ||
|
|
621628e2cf | ||
|
|
ee69290003 | ||
|
|
759e97fd36 | ||
|
|
627023b452 | ||
|
|
83ac8979c2 | ||
|
|
671568aec9 | ||
|
|
47f58212de |
@@ -1,6 +1,6 @@
|
||||
defaults
|
||||
> 0.2%
|
||||
firefox >= 78
|
||||
> 0.2% and not ios < 15.6
|
||||
firefox >= 91
|
||||
ios >= 15.6
|
||||
not dead
|
||||
not OperaMini all
|
||||
|
||||
@@ -59,7 +59,7 @@ body:
|
||||
Any additional technical details you may have, like logs or error traces
|
||||
value: |
|
||||
If this is happening on your own Mastodon server, please fill out those:
|
||||
- Ruby version: (from `ruby --version`, eg. v4.0.2)
|
||||
- Ruby version: (from `ruby --version`, eg. v4.0.5)
|
||||
- Node.js version: (from `node --version`, eg. v22.16.0)
|
||||
validations:
|
||||
required: false
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/3.troubleshooting.yml
vendored
2
.github/ISSUE_TEMPLATE/3.troubleshooting.yml
vendored
@@ -61,7 +61,7 @@ body:
|
||||
value: |
|
||||
Please at least include those informations:
|
||||
- Operating system: (eg. Ubuntu 24.04.2)
|
||||
- Ruby version: (from `ruby --version`, eg. v4.0.2)
|
||||
- Ruby version: (from `ruby --version`, eg. v4.0.5)
|
||||
- Node.js version: (from `node --version`, eg. v22.16.0)
|
||||
validations:
|
||||
required: false
|
||||
|
||||
6
.github/actions/setup-javascript/action.yml
vendored
6
.github/actions/setup-javascript/action.yml
vendored
@@ -9,21 +9,21 @@ runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
# The following is needed because we can not use `cache: true` for `setup-node`, as it does not support Corepack yet and mess up with the cache location if ran after Node is installed
|
||||
- name: Enable corepack
|
||||
shell: bash
|
||||
run: corepack enable
|
||||
run: npm i -g corepack
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
shell: bash
|
||||
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
||||
10
.github/actions/setup-ruby/action.yml
vendored
10
.github/actions/setup-ruby/action.yml
vendored
@@ -14,10 +14,16 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y libicu-dev libidn11-dev libvips42 ${{ inputs.additional-system-dependencies }}
|
||||
sudo apt-get install --no-install-recommends -y \
|
||||
libicu-dev \
|
||||
libidn11-dev \
|
||||
libvips42 \
|
||||
libheif-plugin-aomdec \
|
||||
libheif-plugin-libde265 \
|
||||
${{ inputs.additional-system-dependencies }}
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@c984c1a20bb35a1cbda04477c816cea024418be9 # v1
|
||||
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
|
||||
with:
|
||||
ruby-version: ${{ inputs.ruby-version }}
|
||||
bundler-cache: true
|
||||
|
||||
6
.github/workflows/build-container-image.yml
vendored
6
.github/workflows/build-container-image.yml
vendored
@@ -76,7 +76,7 @@ jobs:
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ inputs.file_to_build }}
|
||||
@@ -87,8 +87,8 @@ jobs:
|
||||
platforms: ${{ matrix.platform }}
|
||||
provenance: false
|
||||
push: ${{ inputs.push_to_images != '' }}
|
||||
cache-from: ${{ inputs.cache && 'type=gha' || '' }}
|
||||
cache-to: ${{ inputs.cache && 'type=gha,mode=max' || '' }}
|
||||
cache-from: ${{ inputs.cache && format('type=gha,scope=build-{0}-{1}', hashFiles(inputs.file_to_build), env.PLATFORM_PAIR) || '' }}
|
||||
cache-to: ${{ inputs.cache && format('type=gha,mode=max,scope=build-{0}-{1}', hashFiles(inputs.file_to_build), env.PLATFORM_PAIR) || '' }}
|
||||
outputs: type=image,"name=${{ env.IMAGE_NAMES }}",push-by-digest=true,name-canonical=true,push=${{ inputs.push_to_images != '' }}
|
||||
|
||||
- name: Export digest
|
||||
|
||||
2
.github/workflows/bundler-audit.yml
vendored
2
.github/workflows/bundler-audit.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@c984c1a20bb35a1cbda04477c816cea024418be9 # v1
|
||||
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
|
||||
2
.github/workflows/chromatic.yml
vendored
2
.github/workflows/chromatic.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
||||
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
|
||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4
|
||||
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -61,6 +61,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
# Download the translation files from Crowdin
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
|
||||
4
.github/workflows/crowdin-download.yml
vendored
4
.github/workflows/crowdin-download.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
# Download the translation files from Crowdin
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
|
||||
# Create or update the pull request
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8
|
||||
with:
|
||||
commit-message: 'New Crowdin translations'
|
||||
title: 'New Crowdin Translations (automated)'
|
||||
|
||||
2
.github/workflows/crowdin-upload.yml
vendored
2
.github/workflows/crowdin-upload.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
2
.github/workflows/lint-css.yml
vendored
2
.github/workflows/lint-css.yml
vendored
@@ -13,7 +13,6 @@ on:
|
||||
- '**/*.css'
|
||||
- '**/*.scss'
|
||||
- '.github/workflows/lint-css.yml'
|
||||
- '.github/stylelint-matcher.json'
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
@@ -24,7 +23,6 @@ on:
|
||||
- '**/*.css'
|
||||
- '**/*.scss'
|
||||
- '.github/workflows/lint-css.yml'
|
||||
- '.github/stylelint-matcher.json'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
||||
4
.github/workflows/lint-haml.yml
vendored
4
.github/workflows/lint-haml.yml
vendored
@@ -6,7 +6,6 @@ on:
|
||||
- 'main'
|
||||
- 'stable-*'
|
||||
paths:
|
||||
- '.github/workflows/haml-lint-problem-matcher.json'
|
||||
- '.github/workflows/lint-haml.yml'
|
||||
- '.haml-lint*.yml'
|
||||
- '.rubocop*.yml'
|
||||
@@ -16,7 +15,6 @@ on:
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/haml-lint-problem-matcher.json'
|
||||
- '.github/workflows/lint-haml.yml'
|
||||
- '.haml-lint*.yml'
|
||||
- '.rubocop*.yml'
|
||||
@@ -36,7 +34,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@c984c1a20bb35a1cbda04477c816cea024418be9 # v1
|
||||
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
|
||||
9
.github/workflows/lint-ruby.yml
vendored
9
.github/workflows/lint-ruby.yml
vendored
@@ -10,7 +10,6 @@ on:
|
||||
- '.rubocop*.yml'
|
||||
- '.ruby-version'
|
||||
- 'bin/rubocop'
|
||||
- 'config/brakeman.ignore'
|
||||
- '**/*.rb'
|
||||
- '**/*.rake'
|
||||
- '.github/workflows/lint-ruby.yml'
|
||||
@@ -21,7 +20,6 @@ on:
|
||||
- '.rubocop*.yml'
|
||||
- '.ruby-version'
|
||||
- 'bin/rubocop'
|
||||
- 'config/brakeman.ignore'
|
||||
- '**/*.rb'
|
||||
- '**/*.rake'
|
||||
- '.github/workflows/lint-ruby.yml'
|
||||
@@ -38,15 +36,12 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@c984c1a20bb35a1cbda04477c816cea024418be9 # v1
|
||||
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Set-up RuboCop Problem Matcher
|
||||
uses: r7kamura/rubocop-problem-matchers-action@59f1a0759f50cc2649849fd850b8487594bb5a81 # v1.2.2
|
||||
|
||||
- name: Run rubocop
|
||||
run: bin/rubocop
|
||||
run: bin/rubocop --format github
|
||||
|
||||
- name: Run brakeman
|
||||
if: always() # Run both checks, even if the first failed
|
||||
|
||||
10
.github/workflows/test-ruby.yml
vendored
10
.github/workflows/test-ruby.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
onlyProduction: 'true'
|
||||
|
||||
- name: Cache assets from compilation
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
public/assets
|
||||
@@ -151,7 +151,7 @@ jobs:
|
||||
bin/flatware fan bin/rails db:test:prepare
|
||||
|
||||
- name: Cache RSpec persistence file
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
tmp/rspec/examples.txt
|
||||
@@ -163,11 +163,11 @@ jobs:
|
||||
rspec-persistence-main
|
||||
rspec-persistence
|
||||
|
||||
- run: bin/flatware rspec -r ./spec/flatware_helper.rb
|
||||
- run: bin/flatware rspec
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: matrix.ruby-version == '.ruby-version'
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6
|
||||
with:
|
||||
files: coverage/lcov/*.lcov
|
||||
env:
|
||||
@@ -247,7 +247,7 @@ jobs:
|
||||
|
||||
- name: Cache Playwright Chromium browser
|
||||
id: playwright-cache
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
@@ -1 +1 @@
|
||||
4.0.2
|
||||
4.0.5
|
||||
|
||||
26
.simplecov
Normal file
26
.simplecov
Normal file
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
SimpleCov.start 'rails' do
|
||||
# During parallel runs, ensure unique names for post-run merge
|
||||
command_name "job-#{ENV['TEST_ENV_NUMBER']}" if ENV['TEST_ENV_NUMBER']
|
||||
|
||||
if ENV['CI']
|
||||
require 'simplecov-lcov'
|
||||
formatter SimpleCov::Formatter::LcovFormatter
|
||||
formatter.config.report_with_single_file = true
|
||||
else
|
||||
formatter SimpleCov::Formatter::HTMLFormatter
|
||||
end
|
||||
|
||||
enable_coverage :branch
|
||||
|
||||
add_filter 'lib/linter'
|
||||
|
||||
add_group 'Libraries', 'lib'
|
||||
add_group 'Policies', 'app/policies'
|
||||
add_group 'Presenters', 'app/presenters'
|
||||
add_group 'Search', 'app/chewy'
|
||||
add_group 'Serializers', 'app/serializers'
|
||||
add_group 'Services', 'app/services'
|
||||
add_group 'Validators', 'app/validators'
|
||||
end
|
||||
33
CHANGELOG.md
33
CHANGELOG.md
@@ -2,6 +2,39 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.5.10] - 2026-05-20
|
||||
|
||||
### Security
|
||||
|
||||
- Fix SSRF protection bypass ([GHSA-crr4-7rm4-8gpw](https://github.com/mastodon/mastodon/security/advisories/GHSA-crr4-7rm4-8gpw), [GHSA-xx55-4rrg-8xg6](https://github.com/mastodon/mastodon/security/advisories/GHSA-xx55-4rrg-8xg6))
|
||||
- Fix Linked-Data Signature bypass through JSON-LD graph restructuring features ([GHSA-53m7-2wrh-q839](https://github.com/mastodon/mastodon/security/advisories/GHSA-53m7-2wrh-q839), [GHSA-chgx-jx3p-rf73](https://github.com/mastodon/mastodon/security/advisories/GHSA-chgx-jx3p-rf73))
|
||||
- Updated dependencies
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix type of `interactingObject`, `interactionTarget` and add missing `QuoteAuthorization` (#38940 by @ClearlyClaire)
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove unused devise strategies (#38795 by @ClearlyClaire)
|
||||
|
||||
## [4.5.9] - 2026-04-15
|
||||
|
||||
### Security
|
||||
|
||||
- Insufficient verification of email addresses ([GHSA-5r37-qpwq-2jhh](https://github.com/mastodon/mastodon/security/advisories/GHSA-5r37-qpwq-2jhh))
|
||||
- Updated dependencies
|
||||
|
||||
### Added
|
||||
|
||||
- Add trademark warning to `mastodon:setup` task (#38548 by @ClearlyClaire)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix definition for `quote` in JSON-LD context (#38686 by @ClearlyClaire)
|
||||
- Fix being unable to disable sound for quote update notification (#38537 by @ClearlyClaire)
|
||||
- Fix being able to quote someone you blocked (#38608 by @ClearlyClaire)
|
||||
|
||||
## [4.5.8] - 2026-03-24
|
||||
|
||||
### Security
|
||||
|
||||
217
Dockerfile
217
Dockerfile
@@ -13,7 +13,7 @@ ARG BASE_REGISTRY="docker.io"
|
||||
|
||||
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="4.0.x"]
|
||||
# renovate: datasource=docker depName=docker.io/ruby
|
||||
ARG RUBY_VERSION="4.0.2"
|
||||
ARG RUBY_VERSION="4.0.5"
|
||||
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="22"]
|
||||
# renovate: datasource=node-version depName=node
|
||||
ARG NODE_MAJOR_VERSION="24"
|
||||
@@ -25,8 +25,8 @@ FROM ${BASE_REGISTRY}/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node
|
||||
FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby
|
||||
|
||||
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
|
||||
# Example: v4.3.0-nightly.2023.11.09+pr-123456
|
||||
# Overwrite existence of 'alpha.X' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"]
|
||||
# Example: v4.3.0-nightly.2023-11-09+pr-123456
|
||||
# Overwrite existence of 'alpha.X' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023-11-09"]
|
||||
ARG MASTODON_VERSION_PRERELEASE=""
|
||||
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="pr-123456"]
|
||||
ARG MASTODON_VERSION_METADATA=""
|
||||
@@ -48,29 +48,27 @@ ARG GID="991"
|
||||
|
||||
# Apply Mastodon build options based on options above
|
||||
ENV \
|
||||
# Apply Mastodon version information
|
||||
MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
|
||||
MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" \
|
||||
SOURCE_COMMIT="${SOURCE_COMMIT}" \
|
||||
# Apply Mastodon static files and YJIT options
|
||||
RAILS_SERVE_STATIC_FILES=${RAILS_SERVE_STATIC_FILES} \
|
||||
RUBY_YJIT_ENABLE=${RUBY_YJIT_ENABLE} \
|
||||
# Apply timezone
|
||||
TZ=${TZ}
|
||||
RAILS_SERVE_STATIC_FILES="${RAILS_SERVE_STATIC_FILES}" \
|
||||
RUBY_YJIT_ENABLE="${RUBY_YJIT_ENABLE}" \
|
||||
TZ="${TZ}"
|
||||
|
||||
# Configure runtime environment
|
||||
# BIND: IP to bind Mastodon to when serving traffic
|
||||
# NODE_ENV/RAILS_ENV: production settings for Node.js and Ruby on Rails
|
||||
# DEBIAN_FRONTEND: suppress interactive prompts
|
||||
# PATH: add Ruby and Mastodon installation directories
|
||||
# MALLOC_CONF: optimize jemalloc 5.x performance
|
||||
# MASTODON_SIDEKIQ_READY_FILENAME: Sidekiq readiness check filename for Kubernetes
|
||||
ENV \
|
||||
# Configure the IP to bind Mastodon to when serving traffic
|
||||
BIND="0.0.0.0" \
|
||||
# Use production settings for Yarn, Node.js and related tools
|
||||
NODE_ENV="production" \
|
||||
# Use production settings for Ruby on Rails
|
||||
RAILS_ENV="production" \
|
||||
# Add Ruby and Mastodon installation to the PATH
|
||||
DEBIAN_FRONTEND="noninteractive" \
|
||||
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" \
|
||||
# Optimize jemalloc 5.x performance
|
||||
MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0" \
|
||||
# Sidekiq will touch tmp/sidekiq_process_has_started_and_will_begin_processing_jobs to indicate it is ready. This can be used for a readiness check in Kubernetes
|
||||
MASTODON_SIDEKIQ_READY_FILENAME=sidekiq_process_has_started_and_will_begin_processing_jobs
|
||||
|
||||
# Set default shell used for running commands
|
||||
@@ -99,10 +97,10 @@ RUN \
|
||||
# Mount Apt cache and lib directories from Docker buildx caches
|
||||
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
|
||||
# Apt update & upgrade to check for security updates to Debian image
|
||||
# Update package list and upgrade system packages
|
||||
apt-get update; \
|
||||
apt-get dist-upgrade -yq; \
|
||||
# Install jemalloc, curl and other necessary components
|
||||
# Install jemalloc and other necessary components
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
file \
|
||||
@@ -112,6 +110,42 @@ RUN \
|
||||
tini \
|
||||
tzdata \
|
||||
wget \
|
||||
# Mastodon components
|
||||
libexpat1 \
|
||||
libglib2.0-0t64 \
|
||||
libicu76 \
|
||||
libidn12 \
|
||||
libpq5 \
|
||||
libreadline8t64 \
|
||||
libssl3t64 \
|
||||
libyaml-0-2 \
|
||||
# libvips components
|
||||
libcgif0 \
|
||||
libexif12 \
|
||||
libheif1 \
|
||||
libhwy1t64 \
|
||||
libimagequant0 \
|
||||
libjpeg62-turbo \
|
||||
liblcms2-2 \
|
||||
libspng0 \
|
||||
libtiff6 \
|
||||
libwebp7 \
|
||||
libwebpdemux2 \
|
||||
libwebpmux3 \
|
||||
# ffmpeg components
|
||||
libdav1d7 \
|
||||
libmp3lame0 \
|
||||
libopencore-amrnb0 \
|
||||
libopencore-amrwb0 \
|
||||
libopus0 \
|
||||
libsnappy1v5 \
|
||||
libtheora0 \
|
||||
libvorbis0a \
|
||||
libvorbisenc2 \
|
||||
libvorbisfile3 \
|
||||
libvpx9 \
|
||||
libx264-164 \
|
||||
libx265-215 \
|
||||
; \
|
||||
# Patch Ruby to use jemalloc
|
||||
patchelf --add-needed libjemalloc.so.2 /usr/local/bin/ruby; \
|
||||
@@ -120,42 +154,37 @@ RUN \
|
||||
patchelf \
|
||||
;
|
||||
|
||||
# Create temporary build layer from base image
|
||||
FROM ruby AS build
|
||||
# Build stage for media libraries (libvips, ffmpeg)
|
||||
FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS media-build
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Set default shell used for running commands
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-c"]
|
||||
|
||||
# hadolint ignore=DL3008
|
||||
RUN \
|
||||
# Mount Apt cache and lib directories from Docker buildx caches
|
||||
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
|
||||
# Install build tools and bundler dependencies from APT
|
||||
--mount=type=cache,id=apt-native-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-native-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
|
||||
# Remove automatic apt cache Docker cleanup scripts
|
||||
rm -f /etc/apt/apt.conf.d/docker-clean; \
|
||||
# Install build tools for native libraries
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
autoconf \
|
||||
automake \
|
||||
build-essential \
|
||||
cmake \
|
||||
git \
|
||||
libgdbm-dev \
|
||||
libglib2.0-dev \
|
||||
libgmp-dev \
|
||||
libicu-dev \
|
||||
libidn-dev \
|
||||
libpq-dev \
|
||||
libssl-dev \
|
||||
libtool \
|
||||
libyaml-dev \
|
||||
meson \
|
||||
nasm \
|
||||
pkg-config \
|
||||
shared-mime-info \
|
||||
xz-utils \
|
||||
# libvips components
|
||||
libcgif-dev \
|
||||
libexif-dev \
|
||||
libexpat1-dev \
|
||||
libgirepository1.0-dev \
|
||||
libglib2.0-dev \
|
||||
libheif-dev \
|
||||
libhwy-dev \
|
||||
libimagequant-dev \
|
||||
@@ -176,8 +205,8 @@ RUN \
|
||||
libx265-dev \
|
||||
;
|
||||
|
||||
# Create temporary libvips specific build layer from build layer
|
||||
FROM build AS libvips
|
||||
# Create temporary libvips specific build layer
|
||||
FROM media-build AS libvips
|
||||
|
||||
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
|
||||
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
|
||||
@@ -192,19 +221,20 @@ RUN tar xf vips-${VIPS_VERSION}.tar.xz;
|
||||
|
||||
WORKDIR /usr/local/libvips/src/vips-${VIPS_VERSION}
|
||||
|
||||
# Configure and compile libvips
|
||||
RUN \
|
||||
meson setup build --prefix /usr/local/libvips --libdir=lib -Ddeprecated=false -Dintrospection=disabled -Dmodules=disabled -Dexamples=false; \
|
||||
cd build; \
|
||||
ninja; \
|
||||
ninja install;
|
||||
# Configure libvips
|
||||
RUN meson setup build --prefix /usr/local/libvips --libdir=lib -Ddeprecated=false -Dintrospection=disabled -Dmodules=disabled -Dexamples=false
|
||||
|
||||
# Create temporary ffmpeg specific build layer from build layer
|
||||
FROM build AS ffmpeg
|
||||
WORKDIR /usr/local/libvips/src/vips-${VIPS_VERSION}/build
|
||||
|
||||
# Compile and install libvips
|
||||
RUN ninja && ninja install
|
||||
|
||||
# Create temporary ffmpeg specific build layer
|
||||
FROM media-build AS ffmpeg
|
||||
|
||||
# ffmpeg version to compile, change with [--build-arg FFMPEG_VERSION="7.0.x"]
|
||||
# renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg
|
||||
ARG FFMPEG_VERSION=8.1
|
||||
# renovate: datasource=github-tags depName=FFmpeg/FFmpeg extractVersion=^n(?<version>\d+\.\d+(\.\d+)?)$
|
||||
ARG FFMPEG_VERSION=8.1.1
|
||||
# ffmpeg download URL, change with [--build-arg FFMPEG_URL="https://ffmpeg.org/releases"]
|
||||
ARG FFMPEG_URL=https://github.com/FFmpeg/FFmpeg/archive/refs/tags
|
||||
|
||||
@@ -241,17 +271,48 @@ RUN \
|
||||
--enable-shared \
|
||||
--enable-version3 \
|
||||
; \
|
||||
make -j$(nproc); \
|
||||
make -j"$(nproc)"; \
|
||||
make install;
|
||||
|
||||
# Create temporary build layer from base image for Ruby dependencies
|
||||
FROM ruby AS ruby-build
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# hadolint ignore=DL3008
|
||||
RUN \
|
||||
# Mount Apt cache and lib directories from Docker buildx caches
|
||||
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
|
||||
# Install build tools and bundler dependencies from APT
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
git \
|
||||
libgdbm-dev \
|
||||
libgmp-dev \
|
||||
libicu-dev \
|
||||
libidn-dev \
|
||||
libpq-dev \
|
||||
libssl-dev \
|
||||
libyaml-dev \
|
||||
shared-mime-info \
|
||||
zlib1g-dev \
|
||||
;
|
||||
|
||||
# Create temporary bundler specific build layer from build layer
|
||||
FROM build AS bundler
|
||||
FROM ruby-build AS bundler
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Copy Gemfile config into working directory
|
||||
COPY Gemfile* /opt/mastodon/
|
||||
|
||||
# Copy libvips for gems that need it during install
|
||||
COPY --from=libvips /usr/local/libvips/lib /usr/local/lib
|
||||
COPY --from=libvips /usr/local/libvips/include /usr/local/include
|
||||
|
||||
RUN ldconfig
|
||||
|
||||
RUN \
|
||||
# Mount Ruby Gem caches
|
||||
--mount=type=cache,id=gem-cache-${TARGETPLATFORM},target=/usr/local/bundle/cache/,sharing=locked \
|
||||
@@ -267,7 +328,7 @@ RUN \
|
||||
bundle install -j"$(nproc)";
|
||||
|
||||
# Create temporary assets build layer from build layer
|
||||
FROM build AS precompiler
|
||||
FROM ruby-build AS precompiler
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
@@ -279,10 +340,13 @@ COPY --from=node /usr/local/bin /usr/local/bin
|
||||
COPY --from=node /usr/local/lib /usr/local/lib
|
||||
|
||||
RUN \
|
||||
# Configure Corepack
|
||||
rm /usr/local/bin/yarn*; \
|
||||
corepack enable; \
|
||||
corepack prepare --activate;
|
||||
# Mount local Corepack and Yarn caches from Docker buildx caches
|
||||
--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
|
||||
--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
|
||||
# Remove pre-installed Yarn binaries (only present on Node <26)
|
||||
rm -f /usr/local/bin/yarn*; \
|
||||
# Install Corepack
|
||||
npm i -g corepack;
|
||||
|
||||
# hadolint ignore=DL3008
|
||||
RUN \
|
||||
@@ -311,53 +375,6 @@ FROM ruby AS mastodon
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# hadolint ignore=DL3008
|
||||
RUN \
|
||||
# Mount Apt cache and lib directories from Docker buildx caches
|
||||
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
|
||||
# Mount Corepack and Yarn caches from Docker buildx caches
|
||||
--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
|
||||
--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
|
||||
# Apt update install non-dev versions of necessary components
|
||||
apt-get install -y --no-install-recommends \
|
||||
libexpat1 \
|
||||
libglib2.0-0t64 \
|
||||
libicu76 \
|
||||
libidn12 \
|
||||
libpq5 \
|
||||
libreadline8t64 \
|
||||
libssl3t64 \
|
||||
libyaml-0-2 \
|
||||
# libvips components
|
||||
libcgif0 \
|
||||
libexif12 \
|
||||
libheif1 \
|
||||
libhwy1t64 \
|
||||
libimagequant0 \
|
||||
libjpeg62-turbo \
|
||||
liblcms2-2 \
|
||||
libspng0 \
|
||||
libtiff6 \
|
||||
libwebp7 \
|
||||
libwebpdemux2 \
|
||||
libwebpmux3 \
|
||||
# ffmpeg components
|
||||
libdav1d7 \
|
||||
libmp3lame0 \
|
||||
libopencore-amrnb0 \
|
||||
libopencore-amrwb0 \
|
||||
libopus0 \
|
||||
libsnappy1v5 \
|
||||
libtheora0 \
|
||||
libvorbis0a \
|
||||
libvorbisenc2 \
|
||||
libvorbisfile3 \
|
||||
libvpx9 \
|
||||
libx264-164 \
|
||||
libx265-215 \
|
||||
;
|
||||
|
||||
# Copy Mastodon sources into final layer
|
||||
COPY . /opt/mastodon/
|
||||
|
||||
|
||||
38
Gemfile
38
Gemfile
@@ -4,7 +4,7 @@ source 'https://rubygems.org'
|
||||
ruby '>= 3.3.0', '< 4.1.0'
|
||||
|
||||
gem 'propshaft'
|
||||
gem 'puma', '~> 7.0'
|
||||
gem 'puma'
|
||||
gem 'rails', '~> 8.1.0'
|
||||
gem 'thor', '~> 1.2'
|
||||
|
||||
@@ -50,7 +50,6 @@ gem 'doorkeeper', '~> 5.6'
|
||||
gem 'faraday-httpclient'
|
||||
gem 'fast_blank', '~> 1.0'
|
||||
gem 'fastimage'
|
||||
gem 'hiredis', '~> 0.6'
|
||||
gem 'hiredis-client'
|
||||
gem 'htmlentities', '~> 4.3'
|
||||
gem 'http', '~> 5.3.0'
|
||||
@@ -59,6 +58,7 @@ gem 'httplog', '~> 1.8.0', require: false
|
||||
gem 'i18n'
|
||||
gem 'idn-ruby', require: 'idn'
|
||||
gem 'inline_svg'
|
||||
gem 'ipaddr', '~> 1.2'
|
||||
gem 'irb', '~> 1.8'
|
||||
gem 'kaminari', '~> 1.2'
|
||||
gem 'link_header', '~> 0.0'
|
||||
@@ -76,7 +76,7 @@ gem 'rack-attack', '~> 6.6'
|
||||
gem 'rack-cors', require: 'rack/cors'
|
||||
gem 'rails-i18n', '~> 8.0'
|
||||
gem 'redcarpet', '~> 3.6'
|
||||
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
|
||||
gem 'redis', '~> 5'
|
||||
gem 'rqrcode', '~> 3.0'
|
||||
gem 'ruby-progressbar', '~> 1.13'
|
||||
gem 'sanitize', '~> 7.0'
|
||||
@@ -102,23 +102,23 @@ gem 'rdf-normalize', '~> 0.5'
|
||||
|
||||
gem 'prometheus_exporter', '~> 2.2', require: false
|
||||
|
||||
gem 'opentelemetry-api', '~> 1.8.0'
|
||||
gem 'opentelemetry-api', '~> 1.10.0'
|
||||
|
||||
group :opentelemetry do
|
||||
gem 'opentelemetry-exporter-otlp', '~> 0.32.0', require: false
|
||||
gem 'opentelemetry-instrumentation-active_job', '~> 0.10.0', require: false
|
||||
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.24.0', require: false
|
||||
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.24.0', require: false
|
||||
gem 'opentelemetry-instrumentation-excon', '~> 0.28.0', require: false
|
||||
gem 'opentelemetry-instrumentation-faraday', '~> 0.32.0', require: false
|
||||
gem 'opentelemetry-instrumentation-http', '~> 0.29.0', require: false
|
||||
gem 'opentelemetry-instrumentation-http_client', '~> 0.28.0', require: false
|
||||
gem 'opentelemetry-instrumentation-net_http', '~> 0.28.0', require: false
|
||||
gem 'opentelemetry-instrumentation-pg', '~> 0.35.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rack', '~> 0.30.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rails', '~> 0.40.0', require: false
|
||||
gem 'opentelemetry-instrumentation-redis', '~> 0.28.0', require: false
|
||||
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.28.0', require: false
|
||||
gem 'opentelemetry-exporter-otlp', '~> 0.34.0', require: false
|
||||
gem 'opentelemetry-instrumentation-active_job', '~> 0.12.0', require: false
|
||||
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.25.0', require: false
|
||||
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.25.0', require: false
|
||||
gem 'opentelemetry-instrumentation-excon', '~> 0.29.0', require: false
|
||||
gem 'opentelemetry-instrumentation-faraday', '~> 0.33.0', require: false
|
||||
gem 'opentelemetry-instrumentation-http', '~> 0.30.0', require: false
|
||||
gem 'opentelemetry-instrumentation-http_client', '~> 0.29.0', require: false
|
||||
gem 'opentelemetry-instrumentation-net_http', '~> 0.29.0', require: false
|
||||
gem 'opentelemetry-instrumentation-pg', '~> 0.36.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rack', '~> 0.31.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rails', '~> 0.42.0', require: false
|
||||
gem 'opentelemetry-instrumentation-redis', '~> 0.29.0', require: false
|
||||
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.29.0', require: false
|
||||
gem 'opentelemetry-sdk', '~> 1.4', require: false
|
||||
end
|
||||
|
||||
@@ -135,7 +135,7 @@ group :test do
|
||||
# Browser integration testing
|
||||
gem 'capybara', '~> 3.39'
|
||||
gem 'capybara-playwright-driver'
|
||||
gem 'playwright-ruby-client', '1.57.1', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package
|
||||
gem 'playwright-ruby-client', '1.59.1', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package
|
||||
|
||||
# Used to reset the database between system tests
|
||||
gem 'database_cleaner-active_record'
|
||||
|
||||
244
Gemfile.lock
244
Gemfile.lock
@@ -89,7 +89,7 @@ GEM
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.9)
|
||||
addressable (2.9.0)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
aes_key_wrap (1.1.0)
|
||||
android_key_attestation (0.3.0)
|
||||
@@ -99,8 +99,8 @@ GEM
|
||||
ast (2.4.3)
|
||||
attr_required (1.0.2)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1227.0)
|
||||
aws-sdk-core (3.244.0)
|
||||
aws-partitions (1.1249.0)
|
||||
aws-sdk-core (3.247.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -108,11 +108,11 @@ GEM
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.123.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sdk-kms (1.125.0)
|
||||
aws-sdk-core (~> 3, >= 3.247.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.217.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sdk-s3 (1.222.0)
|
||||
aws-sdk-core (~> 3, >= 3.247.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
@@ -132,7 +132,7 @@ GEM
|
||||
binding_of_caller (2.0.0)
|
||||
debug_inspector (>= 1.2.0)
|
||||
blurhash (0.1.8)
|
||||
bootsnap (1.23.0)
|
||||
bootsnap (1.24.4)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (8.0.4)
|
||||
racc
|
||||
@@ -156,7 +156,7 @@ GEM
|
||||
playwright-ruby-client (>= 1.16.0)
|
||||
case_transform (0.2)
|
||||
activesupport
|
||||
cbor (0.5.10.1)
|
||||
cbor (0.5.10.2)
|
||||
cgi (0.5.1)
|
||||
charlock_holmes (0.7.9)
|
||||
chewy (8.0.1)
|
||||
@@ -178,7 +178,7 @@ GEM
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
css_parser (1.21.1)
|
||||
css_parser (2.1.0)
|
||||
addressable
|
||||
csv (3.3.5)
|
||||
database_cleaner-active_record (2.2.2)
|
||||
@@ -190,7 +190,7 @@ GEM
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
debug_inspector (1.2.0)
|
||||
devise (5.0.3)
|
||||
devise (5.0.4)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 7.0)
|
||||
@@ -214,7 +214,7 @@ GEM
|
||||
dotenv (3.2.0)
|
||||
drb (2.2.3)
|
||||
dry-cli (1.4.1)
|
||||
elastic-transport (8.4.1)
|
||||
elastic-transport (8.5.1)
|
||||
faraday (< 3)
|
||||
multi_json
|
||||
elasticsearch (8.19.3)
|
||||
@@ -226,14 +226,14 @@ GEM
|
||||
elasticsearch-dsl (0.1.10)
|
||||
email_validator (2.2.4)
|
||||
activemodel
|
||||
erb (6.0.2)
|
||||
erb (6.0.4)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.4.0)
|
||||
tzinfo
|
||||
excon (1.4.0)
|
||||
excon (1.4.2)
|
||||
logger
|
||||
fabrication (3.0.0)
|
||||
faker (3.6.1)
|
||||
faker (3.8.0)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.14.1)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
@@ -247,7 +247,7 @@ GEM
|
||||
net-http (~> 0.5)
|
||||
fast_blank (1.0.1)
|
||||
fastimage (2.4.1)
|
||||
ffi (1.17.3)
|
||||
ffi (1.17.4)
|
||||
ffi-compiler (1.3.2)
|
||||
ffi (>= 1.15.5)
|
||||
rake
|
||||
@@ -264,7 +264,7 @@ GEM
|
||||
excon (~> 1.0)
|
||||
formatador (>= 0.2, < 2.0)
|
||||
mime-types
|
||||
fog-json (1.2.0)
|
||||
fog-json (1.3.0)
|
||||
fog-core
|
||||
multi_json (~> 1.10)
|
||||
fog-openstack (1.1.5)
|
||||
@@ -278,7 +278,7 @@ GEM
|
||||
raabro (~> 1.4)
|
||||
globalid (1.3.0)
|
||||
activesupport (>= 6.1)
|
||||
google-protobuf (4.34.0)
|
||||
google-protobuf (4.34.1)
|
||||
bigdecimal
|
||||
rake (~> 13.3)
|
||||
googleapis-common-protos-types (1.22.0)
|
||||
@@ -292,9 +292,9 @@ GEM
|
||||
activesupport (>= 5.1)
|
||||
haml (>= 4.0.6)
|
||||
railties (>= 5.1)
|
||||
haml_lint (0.72.0)
|
||||
haml_lint (0.73.0)
|
||||
haml (>= 5.0)
|
||||
parallel (~> 1.10)
|
||||
parallel (>= 1.10)
|
||||
rainbow
|
||||
rubocop (>= 1.0)
|
||||
sysexits (~> 1.1)
|
||||
@@ -305,9 +305,8 @@ GEM
|
||||
json
|
||||
highline (3.1.2)
|
||||
reline
|
||||
hiredis (0.6.3)
|
||||
hiredis-client (0.28.0)
|
||||
redis-client (= 0.28.0)
|
||||
hiredis-client (0.29.0)
|
||||
redis-client (= 0.29.0)
|
||||
hkdf (0.3.0)
|
||||
htmlentities (4.4.2)
|
||||
http (5.3.1)
|
||||
@@ -315,7 +314,7 @@ GEM
|
||||
http-cookie (~> 1.0)
|
||||
http-form_data (~> 2.2)
|
||||
llhttp-ffi (~> 0.5.0)
|
||||
http-cookie (1.1.0)
|
||||
http-cookie (1.1.6)
|
||||
domain_name (~> 0.5)
|
||||
http-form_data (2.3.0)
|
||||
http_accept_language (2.1.1)
|
||||
@@ -344,7 +343,8 @@ GEM
|
||||
activesupport (>= 3.0)
|
||||
nokogiri (>= 1.6)
|
||||
io-console (0.8.2)
|
||||
irb (1.17.0)
|
||||
ipaddr (1.2.9)
|
||||
irb (1.18.0)
|
||||
pp (>= 0.6.0)
|
||||
prism (>= 1.3.0)
|
||||
rdoc (>= 4.0.0)
|
||||
@@ -354,7 +354,7 @@ GEM
|
||||
azure-blob (~> 0.5.2)
|
||||
hashie (~> 5.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.19.3)
|
||||
json (2.19.5)
|
||||
json-canonicalization (1.0.0)
|
||||
json-jwt (1.17.0)
|
||||
activesupport (>= 4.2)
|
||||
@@ -412,15 +412,13 @@ GEM
|
||||
rexml
|
||||
link_header (0.0.8)
|
||||
lint_roller (1.1.0)
|
||||
linzer (0.7.8)
|
||||
linzer (0.7.9)
|
||||
cgi (>= 0.4.2, < 0.6.0)
|
||||
forwardable (~> 1.3, >= 1.3.3)
|
||||
logger (~> 1.7, >= 1.7.0)
|
||||
net-http (>= 0.6, < 0.10)
|
||||
openssl (>= 3, < 5)
|
||||
rack (>= 2.2, < 4.0)
|
||||
starry (~> 0.2)
|
||||
stringio (~> 3.1, >= 3.1.2)
|
||||
uri (~> 1.0, >= 1.0.2)
|
||||
llhttp-ffi (0.5.1)
|
||||
ffi-compiler (~> 1.0)
|
||||
@@ -448,18 +446,18 @@ GEM
|
||||
mime-types (3.7.0)
|
||||
logger
|
||||
mime-types-data (~> 3.2025, >= 3.2025.0507)
|
||||
mime-types-data (3.2026.0317)
|
||||
mime-types-data (3.2026.0414)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (6.0.2)
|
||||
minitest (6.0.6)
|
||||
drb (~> 2.0)
|
||||
prism (~> 1.5)
|
||||
msgpack (1.8.0)
|
||||
multi_json (1.19.1)
|
||||
multi_json (1.20.1)
|
||||
mutex_m (0.3.0)
|
||||
net-http (0.6.0)
|
||||
uri
|
||||
net-imap (0.6.3)
|
||||
net-imap (0.6.4)
|
||||
date
|
||||
net-protocol
|
||||
net-ldap (0.20.0)
|
||||
@@ -472,7 +470,7 @@ GEM
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.2)
|
||||
nokogiri (1.19.3)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
omniauth (2.1.4)
|
||||
@@ -509,103 +507,104 @@ GEM
|
||||
openssl (4.0.1)
|
||||
openssl-signature_algorithm (1.3.0)
|
||||
openssl (> 2.0)
|
||||
opentelemetry-api (1.8.0)
|
||||
opentelemetry-api (1.10.0)
|
||||
logger
|
||||
opentelemetry-common (0.23.0)
|
||||
opentelemetry-common (0.25.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-exporter-otlp (0.32.0)
|
||||
opentelemetry-exporter-otlp (0.34.0)
|
||||
google-protobuf (>= 3.18)
|
||||
googleapis-common-protos-types (~> 1.3)
|
||||
opentelemetry-api (~> 1.1)
|
||||
opentelemetry-common (~> 0.20)
|
||||
opentelemetry-sdk (~> 1.10)
|
||||
opentelemetry-semantic_conventions
|
||||
opentelemetry-helpers-sql (0.3.0)
|
||||
opentelemetry-helpers-sql (0.4.0)
|
||||
opentelemetry-api (~> 1.7)
|
||||
opentelemetry-helpers-sql-processor (0.4.0)
|
||||
opentelemetry-helpers-sql-processor (0.5.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-common (~> 0.21)
|
||||
opentelemetry-instrumentation-action_mailer (0.6.1)
|
||||
opentelemetry-instrumentation-action_mailer (0.8.0)
|
||||
opentelemetry-instrumentation-active_support (~> 0.10)
|
||||
opentelemetry-instrumentation-action_pack (0.16.0)
|
||||
opentelemetry-instrumentation-action_pack (0.18.0)
|
||||
opentelemetry-instrumentation-rack (~> 0.29)
|
||||
opentelemetry-instrumentation-action_view (0.11.2)
|
||||
opentelemetry-instrumentation-action_view (0.13.0)
|
||||
opentelemetry-instrumentation-active_support (~> 0.10)
|
||||
opentelemetry-instrumentation-active_job (0.10.1)
|
||||
opentelemetry-instrumentation-active_job (0.12.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-active_model_serializers (0.24.0)
|
||||
opentelemetry-instrumentation-active_model_serializers (0.25.0)
|
||||
opentelemetry-instrumentation-active_support (>= 0.7.0)
|
||||
opentelemetry-instrumentation-active_record (0.11.1)
|
||||
opentelemetry-instrumentation-active_record (0.13.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-active_storage (0.3.1)
|
||||
opentelemetry-instrumentation-active_storage (0.5.0)
|
||||
opentelemetry-instrumentation-active_support (~> 0.10)
|
||||
opentelemetry-instrumentation-active_support (0.10.1)
|
||||
opentelemetry-instrumentation-active_support (0.12.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-base (0.25.0)
|
||||
opentelemetry-instrumentation-base (0.26.0)
|
||||
opentelemetry-api (~> 1.7)
|
||||
opentelemetry-common (~> 0.21)
|
||||
opentelemetry-registry (~> 0.1)
|
||||
opentelemetry-instrumentation-concurrent_ruby (0.24.0)
|
||||
opentelemetry-instrumentation-concurrent_ruby (0.25.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-excon (0.28.0)
|
||||
opentelemetry-instrumentation-excon (0.29.1)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-faraday (0.32.0)
|
||||
opentelemetry-instrumentation-faraday (0.33.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-http (0.29.0)
|
||||
opentelemetry-instrumentation-http (0.30.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-http_client (0.28.0)
|
||||
opentelemetry-instrumentation-http_client (0.29.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-net_http (0.28.0)
|
||||
opentelemetry-instrumentation-net_http (0.29.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-pg (0.35.0)
|
||||
opentelemetry-instrumentation-pg (0.36.0)
|
||||
opentelemetry-helpers-sql
|
||||
opentelemetry-helpers-sql-processor
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-rack (0.30.0)
|
||||
opentelemetry-instrumentation-rack (0.31.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-rails (0.40.0)
|
||||
opentelemetry-instrumentation-action_mailer (~> 0.6)
|
||||
opentelemetry-instrumentation-action_pack (~> 0.15)
|
||||
opentelemetry-instrumentation-action_view (~> 0.11)
|
||||
opentelemetry-instrumentation-active_job (~> 0.10)
|
||||
opentelemetry-instrumentation-active_record (~> 0.11)
|
||||
opentelemetry-instrumentation-active_storage (~> 0.3)
|
||||
opentelemetry-instrumentation-active_support (~> 0.10)
|
||||
opentelemetry-instrumentation-concurrent_ruby (~> 0.23)
|
||||
opentelemetry-instrumentation-redis (0.28.0)
|
||||
opentelemetry-instrumentation-rails (0.42.0)
|
||||
opentelemetry-instrumentation-action_mailer (~> 0.7)
|
||||
opentelemetry-instrumentation-action_pack (~> 0.17)
|
||||
opentelemetry-instrumentation-action_view (~> 0.12)
|
||||
opentelemetry-instrumentation-active_job (~> 0.11)
|
||||
opentelemetry-instrumentation-active_record (~> 0.12)
|
||||
opentelemetry-instrumentation-active_storage (~> 0.4)
|
||||
opentelemetry-instrumentation-active_support (~> 0.11)
|
||||
opentelemetry-instrumentation-concurrent_ruby (~> 0.25)
|
||||
opentelemetry-instrumentation-redis (0.29.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-sidekiq (0.28.1)
|
||||
opentelemetry-instrumentation-sidekiq (0.29.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-registry (0.4.0)
|
||||
opentelemetry-registry (0.6.0)
|
||||
opentelemetry-api (~> 1.1)
|
||||
opentelemetry-sdk (1.10.0)
|
||||
opentelemetry-sdk (1.12.0)
|
||||
logger
|
||||
opentelemetry-api (~> 1.1)
|
||||
opentelemetry-common (~> 0.20)
|
||||
opentelemetry-registry (~> 0.2)
|
||||
opentelemetry-semantic_conventions
|
||||
opentelemetry-semantic_conventions (1.36.0)
|
||||
opentelemetry-semantic_conventions (1.37.1)
|
||||
opentelemetry-api (~> 1.0)
|
||||
orm_adapter (0.5.0)
|
||||
ostruct (0.6.3)
|
||||
ox (2.14.23)
|
||||
ox (2.14.26)
|
||||
bigdecimal (>= 3.0)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.10.2)
|
||||
parallel (1.28.0)
|
||||
parser (3.3.11.1)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
parslet (2.0.0)
|
||||
pastel (0.8.0)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.6.3)
|
||||
pghero (3.7.0)
|
||||
activerecord (>= 7.1)
|
||||
playwright-ruby-client (1.57.1)
|
||||
pghero (3.8.0)
|
||||
activerecord (>= 7.2)
|
||||
playwright-ruby-client (1.59.1)
|
||||
base64
|
||||
concurrent-ruby (>= 1.1.6)
|
||||
mime-types (>= 3.0)
|
||||
pp (0.6.3)
|
||||
prettyprint
|
||||
premailer (1.27.0)
|
||||
premailer (1.29.0)
|
||||
addressable
|
||||
css_parser (>= 1.19.0)
|
||||
htmlentities (>= 4.0.0)
|
||||
@@ -617,7 +616,7 @@ GEM
|
||||
prism (1.9.0)
|
||||
prometheus_exporter (2.3.1)
|
||||
webrick
|
||||
propshaft (1.3.1)
|
||||
propshaft (1.3.2)
|
||||
actionpack (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
rack
|
||||
@@ -625,7 +624,7 @@ GEM
|
||||
date
|
||||
stringio
|
||||
public_suffix (7.0.5)
|
||||
puma (7.2.0)
|
||||
puma (8.0.1)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.5.2)
|
||||
activesupport (>= 3.0.0)
|
||||
@@ -650,7 +649,7 @@ GEM
|
||||
rack (>= 3.0.0, < 4)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-session (2.1.1)
|
||||
rack-session (2.1.2)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.2.0)
|
||||
@@ -691,7 +690,7 @@ GEM
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.1)
|
||||
rake (13.4.2)
|
||||
rdf (3.3.4)
|
||||
bcp47_spec (~> 0.2)
|
||||
bigdecimal (~> 3.1, >= 3.1.5)
|
||||
@@ -708,10 +707,11 @@ GEM
|
||||
readline (0.0.4)
|
||||
reline
|
||||
redcarpet (3.6.1)
|
||||
redis (4.8.1)
|
||||
redis-client (0.28.0)
|
||||
redis (5.4.1)
|
||||
redis-client (>= 0.22.0)
|
||||
redis-client (0.29.0)
|
||||
connection_pool
|
||||
regexp_parser (2.11.3)
|
||||
regexp_parser (2.12.0)
|
||||
reline (0.6.3)
|
||||
io-console (~> 0.5)
|
||||
request_store (1.7.0)
|
||||
@@ -769,10 +769,10 @@ GEM
|
||||
rubocop-ast (1.49.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.7)
|
||||
rubocop-capybara (2.22.1)
|
||||
rubocop-capybara (2.23.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop-i18n (3.2.3)
|
||||
rubocop (~> 1.81)
|
||||
rubocop-i18n (3.3.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.72.1)
|
||||
rubocop-performance (1.26.1)
|
||||
@@ -802,7 +802,7 @@ GEM
|
||||
ruby-vips (2.3.0)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
rubyzip (3.2.2)
|
||||
rubyzip (3.3.0)
|
||||
rufus-scheduler (3.9.2)
|
||||
fugit (~> 1.1, >= 1.11.1)
|
||||
safety_net_attestation (0.5.0)
|
||||
@@ -816,15 +816,15 @@ GEM
|
||||
securerandom (0.4.1)
|
||||
shoulda-matchers (7.0.1)
|
||||
activesupport (>= 7.1)
|
||||
sidekiq (8.0.10)
|
||||
connection_pool (>= 2.5.0)
|
||||
json (>= 2.9.0)
|
||||
logger (>= 1.6.2)
|
||||
rack (>= 3.1.0)
|
||||
redis-client (>= 0.23.2)
|
||||
sidekiq (8.1.5)
|
||||
connection_pool (>= 3.0.0)
|
||||
json (>= 2.16.0)
|
||||
logger (>= 1.7.0)
|
||||
rack (>= 3.2.0)
|
||||
redis-client (>= 0.29.0)
|
||||
sidekiq-bulk (0.2.0)
|
||||
sidekiq
|
||||
sidekiq-scheduler (6.0.1)
|
||||
sidekiq-scheduler (6.0.2)
|
||||
rufus-scheduler (~> 3.2)
|
||||
sidekiq (>= 7.3, < 9)
|
||||
sidekiq-unique-jobs (8.0.13)
|
||||
@@ -847,12 +847,12 @@ GEM
|
||||
stackprof (0.2.28)
|
||||
starry (0.2.0)
|
||||
base64
|
||||
stoplight (5.8.0)
|
||||
stoplight (5.8.2)
|
||||
concurrent-ruby
|
||||
zeitwerk
|
||||
stringio (3.2.0)
|
||||
strong_migrations (2.5.2)
|
||||
activerecord (>= 7.1)
|
||||
strong_migrations (2.8.0)
|
||||
activerecord (>= 7.2)
|
||||
swd (2.0.3)
|
||||
activesupport (>= 3)
|
||||
attr_required (>= 0.0.5)
|
||||
@@ -864,7 +864,7 @@ GEM
|
||||
unicode-display_width (>= 1.1.1, < 4)
|
||||
terrapin (1.1.1)
|
||||
climate_control
|
||||
test-prof (1.6.0)
|
||||
test-prof (1.6.1)
|
||||
thor (1.5.0)
|
||||
tilt (2.7.0)
|
||||
timeout (0.6.1)
|
||||
@@ -888,7 +888,7 @@ GEM
|
||||
unf (~> 0.1.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2026.1)
|
||||
tzinfo-data (1.2026.2)
|
||||
tzinfo (>= 1.0.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
@@ -904,7 +904,7 @@ GEM
|
||||
vite_rails (3.0.20)
|
||||
railties (>= 5.1, < 9)
|
||||
vite_ruby (~> 3.0, >= 3.2.2)
|
||||
vite_ruby (3.9.3)
|
||||
vite_ruby (3.10.2)
|
||||
dry-cli (>= 0.7, < 2)
|
||||
logger (~> 1.6)
|
||||
mutex_m
|
||||
@@ -984,7 +984,6 @@ DEPENDENCIES
|
||||
haml-rails (~> 3.0)
|
||||
haml_lint
|
||||
hcaptcha (~> 7.1)
|
||||
hiredis (~> 0.6)
|
||||
hiredis-client
|
||||
htmlentities (~> 4.3)
|
||||
http (~> 5.3.0)
|
||||
@@ -994,6 +993,7 @@ DEPENDENCIES
|
||||
i18n-tasks (~> 1.0)
|
||||
idn-ruby
|
||||
inline_svg
|
||||
ipaddr (~> 1.2)
|
||||
irb (~> 1.8)
|
||||
jd-paperclip-azure (~> 3.0)
|
||||
json
|
||||
@@ -1020,32 +1020,32 @@ DEPENDENCIES
|
||||
omniauth-rails_csrf_protection (~> 2.0)
|
||||
omniauth-saml (~> 2.0)
|
||||
omniauth_openid_connect (~> 0.8.0)
|
||||
opentelemetry-api (~> 1.8.0)
|
||||
opentelemetry-exporter-otlp (~> 0.32.0)
|
||||
opentelemetry-instrumentation-active_job (~> 0.10.0)
|
||||
opentelemetry-instrumentation-active_model_serializers (~> 0.24.0)
|
||||
opentelemetry-instrumentation-concurrent_ruby (~> 0.24.0)
|
||||
opentelemetry-instrumentation-excon (~> 0.28.0)
|
||||
opentelemetry-instrumentation-faraday (~> 0.32.0)
|
||||
opentelemetry-instrumentation-http (~> 0.29.0)
|
||||
opentelemetry-instrumentation-http_client (~> 0.28.0)
|
||||
opentelemetry-instrumentation-net_http (~> 0.28.0)
|
||||
opentelemetry-instrumentation-pg (~> 0.35.0)
|
||||
opentelemetry-instrumentation-rack (~> 0.30.0)
|
||||
opentelemetry-instrumentation-rails (~> 0.40.0)
|
||||
opentelemetry-instrumentation-redis (~> 0.28.0)
|
||||
opentelemetry-instrumentation-sidekiq (~> 0.28.0)
|
||||
opentelemetry-api (~> 1.10.0)
|
||||
opentelemetry-exporter-otlp (~> 0.34.0)
|
||||
opentelemetry-instrumentation-active_job (~> 0.12.0)
|
||||
opentelemetry-instrumentation-active_model_serializers (~> 0.25.0)
|
||||
opentelemetry-instrumentation-concurrent_ruby (~> 0.25.0)
|
||||
opentelemetry-instrumentation-excon (~> 0.29.0)
|
||||
opentelemetry-instrumentation-faraday (~> 0.33.0)
|
||||
opentelemetry-instrumentation-http (~> 0.30.0)
|
||||
opentelemetry-instrumentation-http_client (~> 0.29.0)
|
||||
opentelemetry-instrumentation-net_http (~> 0.29.0)
|
||||
opentelemetry-instrumentation-pg (~> 0.36.0)
|
||||
opentelemetry-instrumentation-rack (~> 0.31.0)
|
||||
opentelemetry-instrumentation-rails (~> 0.42.0)
|
||||
opentelemetry-instrumentation-redis (~> 0.29.0)
|
||||
opentelemetry-instrumentation-sidekiq (~> 0.29.0)
|
||||
opentelemetry-sdk (~> 1.4)
|
||||
ox (~> 2.14)
|
||||
parslet
|
||||
pg (~> 1.5)
|
||||
pghero
|
||||
playwright-ruby-client (= 1.57.1)
|
||||
playwright-ruby-client (= 1.59.1)
|
||||
premailer-rails
|
||||
prometheus_exporter (~> 2.2)
|
||||
propshaft
|
||||
public_suffix (~> 7.0)
|
||||
puma (~> 7.0)
|
||||
puma
|
||||
pundit (~> 2.3)
|
||||
rack-attack (~> 6.6)
|
||||
rack-cors
|
||||
@@ -1054,7 +1054,7 @@ DEPENDENCIES
|
||||
rails-i18n (~> 8.0)
|
||||
rdf-normalize (~> 0.5)
|
||||
redcarpet (~> 3.6)
|
||||
redis (~> 4.5)
|
||||
redis (~> 5)
|
||||
rqrcode (~> 3.0)
|
||||
rspec-github (~> 3.0)
|
||||
rspec-rails (~> 8.0)
|
||||
@@ -1097,7 +1097,7 @@ DEPENDENCIES
|
||||
xorcist (~> 1.1)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 4.0.2
|
||||
ruby 4.0.5
|
||||
|
||||
BUNDLED WITH
|
||||
4.0.8
|
||||
4.0.11
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<p align="center">
|
||||
<a style="text-decoration:none" href="https://www.youtube.com/watch?v=IPSbNdBmWKE">
|
||||
<img alt="Mastodon hero image" src="https://github.com/user-attachments/assets/ef53f5e9-c0d8-484d-9f53-00efdebb92c3" />
|
||||
<img alt="Mastodon hero image" src="./docs/hero-nodes.gif" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -59,7 +59,7 @@ Mastodon is a **free, open-source social network server** based on [ActivityPub]
|
||||
- **Ruby** 3.3+
|
||||
- **PostgreSQL** 14+
|
||||
- **Redis** 7.0+
|
||||
- **Node.js** 20+
|
||||
- **Node.js** 22+
|
||||
- **FFmpeg** 5.1+
|
||||
|
||||
This repository includes deployment configurations for **Docker and docker-compose**, as well as for other environments like Heroku and Scalingo. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). A [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the main documentation.
|
||||
|
||||
4
Vagrantfile
vendored
4
Vagrantfile
vendored
@@ -12,7 +12,7 @@ sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main'
|
||||
# Add repo for NodeJS
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||
NODE_MAJOR=20
|
||||
NODE_MAJOR=24
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
|
||||
sudo apt-get update
|
||||
|
||||
@@ -115,7 +115,7 @@ gem install bundler foreman
|
||||
bundle install
|
||||
|
||||
# Install node modules
|
||||
sudo corepack enable
|
||||
sudo npm i -g corepack
|
||||
corepack prepare
|
||||
yarn install
|
||||
|
||||
|
||||
@@ -4,13 +4,48 @@ module Admin
|
||||
class CollectionsController < BaseController
|
||||
before_action :set_account
|
||||
before_action :set_collection, only: :show
|
||||
before_action :set_collections, except: :show
|
||||
|
||||
PER_PAGE = 20
|
||||
|
||||
def index
|
||||
authorize [:admin, :collection], :index?
|
||||
@collection_batch_action = Admin::CollectionBatchAction.new
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @collection, :show?
|
||||
end
|
||||
|
||||
def batch
|
||||
authorize [:admin, :collection], :index?
|
||||
|
||||
@collection_batch_action = Admin::CollectionBatchAction.new(admin_collection_batch_action_params.merge(current_account: current_account, report_id: params[:report_id], type: action_from_button))
|
||||
|
||||
@collection_batch_action.save!
|
||||
rescue ActionController::ParameterMissing
|
||||
flash[:alert] = I18n.t('admin.collections.no_collection_selected')
|
||||
ensure
|
||||
redirect_to after_create_redirect_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def after_create_redirect_path
|
||||
report_id = @collections_batch_action&.report_id || params[:report_id]
|
||||
|
||||
if report_id.present?
|
||||
admin_report_path(report_id)
|
||||
else
|
||||
admin_account_collections_path(params[:account_id], params[:page])
|
||||
end
|
||||
end
|
||||
|
||||
def admin_collection_batch_action_params
|
||||
params
|
||||
.expect(admin_collection_batch_action: [collection_ids: []])
|
||||
end
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
@@ -18,5 +53,17 @@ module Admin
|
||||
def set_collection
|
||||
@collection = @account.collections.includes(accepted_collection_items: :account).find(params[:id])
|
||||
end
|
||||
|
||||
def set_collections
|
||||
@collections = @account.collections.includes(accepted_collection_items: :account).page(params[:page]).per(PER_PAGE)
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
if params[:report]
|
||||
'report'
|
||||
elsif params[:remove_from_report]
|
||||
'remove_from_report'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ module Admin
|
||||
def index
|
||||
authorize :email_domain_block, :index?
|
||||
|
||||
@email_domain_blocks = EmailDomainBlock.parents.includes(:children).order(id: :desc).page(params[:page])
|
||||
@email_domain_blocks = filter_by_domain.page(params[:page])
|
||||
@form = Form::EmailDomainBlockBatch.new
|
||||
end
|
||||
|
||||
@@ -57,6 +57,12 @@ module Admin
|
||||
|
||||
private
|
||||
|
||||
def filter_by_domain
|
||||
scope = EmailDomainBlock.parents.includes(:children).order(id: :desc)
|
||||
scope.merge!(EmailDomainBlock.matches_domain(params[:domain])) if params[:domain].present?
|
||||
scope
|
||||
end
|
||||
|
||||
def set_resolved_records
|
||||
@resolved_records = DomainResource.new(@email_domain_block.domain).mx
|
||||
end
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Admin::EmailSubscriptions::AdditionalFooterTextsController < Admin::SettingsController
|
||||
private
|
||||
|
||||
def after_update_redirect_path
|
||||
admin_email_subscriptions_path
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Admin::EmailSubscriptions::SetupsController < Admin::BaseController
|
||||
before_action :require_enabled!
|
||||
|
||||
def show
|
||||
authorize :email_subscription, :enable?
|
||||
|
||||
@form = Form::EmailSubscriptionsConfirmation.new
|
||||
end
|
||||
|
||||
def create
|
||||
authorize :email_subscription, :enable?
|
||||
|
||||
@form = Form::EmailSubscriptionsConfirmation.new(resource_params)
|
||||
|
||||
if @form.valid?
|
||||
Setting.email_subscriptions = true
|
||||
redirect_to admin_email_subscriptions_path
|
||||
else
|
||||
render :show
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def require_enabled!
|
||||
raise ActionController::RoutingError, 'Feature disabled' unless Rails.application.config.x.email_subscriptions
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.expect(form_email_subscriptions_confirmation: [:agreement_email_volume, :agreement_privacy_and_terms])
|
||||
end
|
||||
end
|
||||
23
app/controllers/admin/email_subscriptions_controller.rb
Normal file
23
app/controllers/admin/email_subscriptions_controller.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Admin::EmailSubscriptionsController < Admin::BaseController
|
||||
def index
|
||||
authorize :email_subscription, :index?
|
||||
|
||||
@enabled = Setting.email_subscriptions
|
||||
@roles = UserRole.where('permissions & ? != 0', UserRole::FLAGS[:manage_email_subscriptions] | UserRole::FLAGS[:administrator])
|
||||
@accounts = Account.local.where.associated(:email_subscriptions).includes(:user)
|
||||
end
|
||||
|
||||
def disable
|
||||
authorize :email_subscription, :disable?
|
||||
Setting.email_subscriptions = false
|
||||
redirect_to admin_email_subscriptions_path, notice: I18n.t('admin.email_subscriptions.disabled_msg')
|
||||
end
|
||||
|
||||
def purge
|
||||
authorize :email_subscription, :purge?
|
||||
Admin::EmailSubscriptionsPurgeWorker.perform_async
|
||||
redirect_to admin_email_subscriptions_path, notice: I18n.t('admin.email_subscriptions.purged_msg')
|
||||
end
|
||||
end
|
||||
@@ -16,6 +16,8 @@ module Admin
|
||||
@report_notes = @report.notes.chronological.includes(:account)
|
||||
@action_logs = @report.history.includes(:target)
|
||||
@form = Admin::StatusBatchAction.new
|
||||
@collection_form = Admin::CollectionBatchAction.new
|
||||
@collections = @report.collections
|
||||
@statuses = @report.statuses.with_includes
|
||||
end
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ module Admin
|
||||
|
||||
def resource_params
|
||||
params
|
||||
.expect(user_role: [:name, :color, :highlighted, :position, :require_2fa, permissions_as_keys: []])
|
||||
.expect(user_role: [:name, :color, :highlighted, :position, :require_2fa, :collection_limit, permissions_as_keys: []])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,7 +19,7 @@ class Api::V1::Accounts::EmailSubscriptionsController < Api::BaseController
|
||||
end
|
||||
|
||||
def require_feature_enabled!
|
||||
head 404 unless Mastodon::Feature.email_subscriptions_enabled?
|
||||
head 404 unless Rails.application.config.x.email_subscriptions && Setting.email_subscriptions
|
||||
end
|
||||
|
||||
def require_account_permissions!
|
||||
|
||||
@@ -16,7 +16,7 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||
@relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
|
||||
end
|
||||
|
||||
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: @relationships
|
||||
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: @relationships, supported_notification_types: params[:supported_types]
|
||||
end
|
||||
|
||||
def unread_count
|
||||
@@ -29,7 +29,7 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||
|
||||
def show
|
||||
@notification = current_account.notifications.without_suspended.find(params[:id])
|
||||
render json: @notification, serializer: REST::NotificationSerializer
|
||||
render json: @notification, serializer: REST::NotificationSerializer, supported_notification_types: params[:supported_types]
|
||||
end
|
||||
|
||||
def clear
|
||||
@@ -89,6 +89,8 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:limit, :account_id, :types, :exclude_types, :include_filtered).permit(:limit, :account_id, :include_filtered, types: [], exclude_types: []).merge(core_params)
|
||||
params.slice(:limit, :account_id, :types, :exclude_types, :include_filtered, :supported_types)
|
||||
.permit(:limit, :account_id, :include_filtered, types: [], exclude_types: [], supported_types: [])
|
||||
.merge(core_params)
|
||||
end
|
||||
end
|
||||
|
||||
67
app/controllers/api/v1/statuses/contexts_controller.rb
Normal file
67
app/controllers/api/v1/statuses/contexts_controller.rb
Normal file
@@ -0,0 +1,67 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Statuses::ContextsController < Api::BaseController
|
||||
include Authorization
|
||||
include AsyncRefreshesConcern
|
||||
|
||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
|
||||
before_action :set_status
|
||||
|
||||
# This API was originally unlimited, pagination cannot be introduced without
|
||||
# breaking backwards-compatibility. Arbitrarily high number to cover most
|
||||
# conversations as quasi-unlimited, it would be too much work to render more
|
||||
# than this anyway
|
||||
CONTEXT_LIMIT = 4_096
|
||||
|
||||
# This remains expensive and we don't want to show everything to logged-out users
|
||||
ANCESTORS_LIMIT = 40
|
||||
DESCENDANTS_LIMIT = 60
|
||||
DESCENDANTS_DEPTH_LIMIT = 20
|
||||
|
||||
def show
|
||||
cache_if_unauthenticated!
|
||||
|
||||
ancestors_limit = CONTEXT_LIMIT
|
||||
descendants_limit = CONTEXT_LIMIT
|
||||
descendants_depth_limit = nil
|
||||
|
||||
if current_account.nil?
|
||||
ancestors_limit = ANCESTORS_LIMIT
|
||||
descendants_limit = DESCENDANTS_LIMIT
|
||||
descendants_depth_limit = DESCENDANTS_DEPTH_LIMIT
|
||||
end
|
||||
|
||||
ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(ancestors_limit, current_account)
|
||||
descendants_results = @status.descendants(descendants_limit, current_account, descendants_depth_limit)
|
||||
loaded_ancestors = preload_collection(ancestors_results, Status)
|
||||
loaded_descendants = preload_collection(descendants_results, Status)
|
||||
|
||||
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
|
||||
statuses = [@status] + @context.ancestors + @context.descendants
|
||||
|
||||
refresh_key = "context:#{@status.id}:refresh"
|
||||
async_refresh = AsyncRefresh.new(refresh_key)
|
||||
|
||||
if async_refresh.running?
|
||||
add_async_refresh_header(async_refresh)
|
||||
elsif !current_account.nil? && @status.should_fetch_replies?
|
||||
add_async_refresh_header(AsyncRefresh.create(refresh_key, count_results: true))
|
||||
|
||||
WorkerBatch.new.within do |batch|
|
||||
batch.connect(refresh_key, threshold: 1.0)
|
||||
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id, { 'batch_id' => batch.id })
|
||||
end
|
||||
end
|
||||
|
||||
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
class Api::V1::StatusesController < Api::BaseController
|
||||
include Authorization
|
||||
include AsyncRefreshesConcern
|
||||
include Api::InteractionPoliciesConcern
|
||||
|
||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
|
||||
before_action :require_user!, except: [:index, :show, :context]
|
||||
before_action :require_user!, except: [:index, :show]
|
||||
before_action :set_statuses, only: [:index]
|
||||
before_action :set_status, only: [:show, :context]
|
||||
before_action :set_status, only: [:show]
|
||||
before_action :set_thread, only: [:create]
|
||||
before_action :set_quoted_status, only: [:create]
|
||||
before_action :check_statuses_limit, only: [:index]
|
||||
@@ -17,17 +16,6 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
override_rate_limit_headers :create, family: :statuses
|
||||
override_rate_limit_headers :update, family: :statuses
|
||||
|
||||
# This API was originally unlimited, pagination cannot be introduced without
|
||||
# breaking backwards-compatibility. Arbitrarily high number to cover most
|
||||
# conversations as quasi-unlimited, it would be too much work to render more
|
||||
# than this anyway
|
||||
CONTEXT_LIMIT = 4_096
|
||||
|
||||
# This remains expensive and we don't want to show everything to logged-out users
|
||||
ANCESTORS_LIMIT = 40
|
||||
DESCENDANTS_LIMIT = 60
|
||||
DESCENDANTS_DEPTH_LIMIT = 20
|
||||
|
||||
def index
|
||||
@statuses = preload_collection(@statuses, Status)
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer
|
||||
@@ -39,44 +27,6 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
render json: @status, serializer: REST::StatusSerializer
|
||||
end
|
||||
|
||||
def context
|
||||
cache_if_unauthenticated!
|
||||
|
||||
ancestors_limit = CONTEXT_LIMIT
|
||||
descendants_limit = CONTEXT_LIMIT
|
||||
descendants_depth_limit = nil
|
||||
|
||||
if current_account.nil?
|
||||
ancestors_limit = ANCESTORS_LIMIT
|
||||
descendants_limit = DESCENDANTS_LIMIT
|
||||
descendants_depth_limit = DESCENDANTS_DEPTH_LIMIT
|
||||
end
|
||||
|
||||
ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(ancestors_limit, current_account)
|
||||
descendants_results = @status.descendants(descendants_limit, current_account, descendants_depth_limit)
|
||||
loaded_ancestors = preload_collection(ancestors_results, Status)
|
||||
loaded_descendants = preload_collection(descendants_results, Status)
|
||||
|
||||
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
|
||||
statuses = [@status] + @context.ancestors + @context.descendants
|
||||
|
||||
refresh_key = "context:#{@status.id}:refresh"
|
||||
async_refresh = AsyncRefresh.new(refresh_key)
|
||||
|
||||
if async_refresh.running?
|
||||
add_async_refresh_header(async_refresh)
|
||||
elsif !current_account.nil? && @status.should_fetch_replies?
|
||||
add_async_refresh_header(AsyncRefresh.create(refresh_key, count_results: true))
|
||||
|
||||
WorkerBatch.new.within do |batch|
|
||||
batch.connect(refresh_key, threshold: 1.0)
|
||||
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id, { 'batch_id' => batch.id })
|
||||
end
|
||||
end
|
||||
|
||||
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
||||
end
|
||||
|
||||
def create
|
||||
@status = PostStatusService.new.call(
|
||||
current_user.account,
|
||||
|
||||
63
app/controllers/api/v1_alpha/in_collections_controller.rb
Normal file
63
app/controllers/api/v1_alpha/in_collections_controller.rb
Normal file
@@ -0,0 +1,63 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1Alpha::InCollectionsController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
DEFAULT_COLLECTIONS_LIMIT = 40
|
||||
|
||||
before_action :check_feature_enabled
|
||||
|
||||
before_action -> { authorize_if_got_token! :read, :'read:collections' }, only: [:index]
|
||||
|
||||
before_action :require_user!
|
||||
before_action :set_account, only: [:index]
|
||||
before_action :set_collections, only: [:index]
|
||||
|
||||
after_action :insert_pagination_headers, only: [:index]
|
||||
|
||||
after_action :verify_authorized
|
||||
|
||||
def index
|
||||
cache_if_unauthenticated!
|
||||
authorize @account, :index_featured_in_collections?
|
||||
|
||||
render json: @collections, each_serializer: REST::CollectionSerializer, adapter: :json
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
|
||||
def set_collections
|
||||
@collections = @account.featured_in_collections
|
||||
.with_tag
|
||||
.offset(offset_param)
|
||||
.limit(limit_param(DEFAULT_COLLECTIONS_LIMIT))
|
||||
end
|
||||
|
||||
def check_feature_enabled
|
||||
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
|
||||
end
|
||||
|
||||
def next_path
|
||||
return unless records_continue?
|
||||
|
||||
api_v1_alpha_account_in_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT)))
|
||||
end
|
||||
|
||||
def prev_path
|
||||
return if offset_param.zero?
|
||||
|
||||
api_v1_alpha_account_in_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT)))
|
||||
end
|
||||
|
||||
def records_continue?
|
||||
((offset_param * limit_param(DEFAULT_COLLECTIONS_LIMIT)) + @collections.size) < @account.featured_in_collections.size
|
||||
end
|
||||
|
||||
def offset_param
|
||||
params[:offset].to_i
|
||||
end
|
||||
end
|
||||
@@ -33,7 +33,7 @@ class Api::V2::NotificationsController < Api::BaseController
|
||||
'app.notification_grouping.expand_accounts_param' => expand_accounts_param
|
||||
)
|
||||
|
||||
render json: @presenter, serializer: REST::DedupNotificationGroupSerializer, relationships: @relationships, expand_accounts: expand_accounts_param
|
||||
render json: @presenter, serializer: REST::DedupNotificationGroupSerializer, relationships: @relationships, expand_accounts: expand_accounts_param, supported_notification_types: params[:supported_types]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,7 +48,7 @@ class Api::V2::NotificationsController < Api::BaseController
|
||||
def show
|
||||
@notification = current_account.notifications.without_suspended.by_group_key(params[:group_key]).take!
|
||||
presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification]))
|
||||
render json: presenter, serializer: REST::DedupNotificationGroupSerializer
|
||||
render json: presenter, serializer: REST::DedupNotificationGroupSerializer, supported_notification_types: params[:supported_types]
|
||||
end
|
||||
|
||||
def clear
|
||||
@@ -138,7 +138,9 @@ class Api::V2::NotificationsController < Api::BaseController
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:limit, :include_filtered, :types, :exclude_types, :grouped_types).permit(:limit, :include_filtered, types: [], exclude_types: [], grouped_types: []).merge(core_params)
|
||||
params.slice(:limit, :include_filtered, :types, :exclude_types, :grouped_types, :supported_types)
|
||||
.permit(:limit, :include_filtered, types: [], exclude_types: [], grouped_types: [], supported_types: [])
|
||||
.merge(core_params)
|
||||
end
|
||||
|
||||
def expand_accounts_param
|
||||
|
||||
@@ -17,7 +17,10 @@ class CollectionsController < ApplicationController
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
# TODO: format.html
|
||||
format.html do
|
||||
expires_in expiration_duration, public: true unless user_signed_in?
|
||||
render template: 'home/index'
|
||||
end
|
||||
|
||||
format.json do
|
||||
expires_in expiration_duration, public: true if public_fetch_mode?
|
||||
@@ -28,8 +31,17 @@ class CollectionsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
if account_id_param.present?
|
||||
@account = Account.local.find(account_id_param)
|
||||
else
|
||||
@collection = Collection.find(params[:id])
|
||||
@account = @collection.account
|
||||
end
|
||||
end
|
||||
|
||||
def set_collection
|
||||
@collection = @account.collections.find(params[:id])
|
||||
@collection ||= @account.collections.find(params[:id])
|
||||
authorize @collection, :show?
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
|
||||
@@ -142,6 +142,7 @@ module SignatureVerification
|
||||
# Only refreshing keys, skipping potentially more expensive requests
|
||||
ActivityPub::FetchRemoteActorService.new.call(keypair.actor.uri, only_key: true, suppress_errors: false)
|
||||
end
|
||||
return if actor.nil?
|
||||
|
||||
keypair_uri = keypair.uri
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class CustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController
|
||||
def show
|
||||
expires_in 1.month, public: true
|
||||
render content_type: 'text/css'
|
||||
render content_type: :css
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -24,12 +24,7 @@ class MediaController < ApplicationController
|
||||
private
|
||||
|
||||
def set_media_attachment
|
||||
id = params[:id] || params[:medium_id]
|
||||
return if id.nil?
|
||||
|
||||
scope = MediaAttachment.local.attached
|
||||
# If id is 19 characters long, it's a shortcode, otherwise it's an identifier
|
||||
@media_attachment = id.size == 19 ? scope.find_by!(shortcode: id) : scope.find(id)
|
||||
@media_attachment = MediaAttachment.local.attached.identified(params[:id])
|
||||
end
|
||||
|
||||
def verify_permitted_status!
|
||||
|
||||
10
app/controllers/redirect/collections_controller.rb
Normal file
10
app/controllers/redirect/collections_controller.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Redirect::CollectionsController < Redirect::BaseController
|
||||
private
|
||||
|
||||
def set_resource
|
||||
@resource = Collection.find(params[:id])
|
||||
not_found if @resource.local? || @resource&.account&.suspended?
|
||||
end
|
||||
end
|
||||
@@ -7,7 +7,7 @@ module Settings
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :require_challenge!, on: :create
|
||||
before_action :require_challenge!
|
||||
|
||||
def create
|
||||
@recovery_codes = current_user.generate_otp_backup_codes!
|
||||
|
||||
@@ -127,6 +127,10 @@ module ApplicationHelper
|
||||
)
|
||||
end
|
||||
|
||||
def emptyphaunt
|
||||
inline_svg_tag 'elephant_ui.svg'
|
||||
end
|
||||
|
||||
def check_icon
|
||||
inline_svg_tag 'check.svg'
|
||||
end
|
||||
|
||||
@@ -34,7 +34,7 @@ module ContextHelper
|
||||
},
|
||||
quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' },
|
||||
quotes: {
|
||||
'quote' => 'https://w3id.org/fep/044f#quote',
|
||||
'quote' => { '@id' => 'https://w3id.org/fep/044f#quote', '@type' => '@id' },
|
||||
'quoteUri' => 'http://fedibird.com/ns#quoteUri',
|
||||
'_misskey_quote' => 'https://misskey-hub.net/ns#_misskey_quote',
|
||||
'quoteAuthorization' => { '@id' => 'https://w3id.org/fep/044f#quoteAuthorization', '@type' => '@id' },
|
||||
@@ -48,9 +48,9 @@ module ContextHelper
|
||||
},
|
||||
quote_authorizations: {
|
||||
'gts' => 'https://gotosocial.org/ns#',
|
||||
'quoteAuthorization' => { '@id' => 'https://w3id.org/fep/044f#quoteAuthorization', '@type' => '@id' },
|
||||
'interactingObject' => { '@id' => 'gts:interactingObject' },
|
||||
'interactionTarget' => { '@id' => 'gts:interactionTarget' },
|
||||
'QuoteAuthorization' => 'https://w3id.org/fep/044f#QuoteAuthorization',
|
||||
'interactingObject' => { '@id' => 'gts:interactingObject', '@type' => '@id' },
|
||||
'interactionTarget' => { '@id' => 'gts:interactionTarget', '@type' => '@id' },
|
||||
},
|
||||
}.freeze
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module InvitesHelper
|
||||
def invites_max_uses_options
|
||||
[1, 5, 10, 25, 50, 100]
|
||||
end
|
||||
|
||||
def invites_expires_options
|
||||
[30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week]
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,8 @@
|
||||
module JsonLdHelper
|
||||
include ContextHelper
|
||||
|
||||
UNSUPPORTED_JSONLD_KEYWORDS = %w(@graph @included @reverse).freeze
|
||||
|
||||
def equals_or_includes?(haystack, needle)
|
||||
haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle
|
||||
end
|
||||
@@ -110,6 +112,16 @@ module JsonLdHelper
|
||||
compacted
|
||||
end
|
||||
|
||||
def unsupported_jsonld_features?(json)
|
||||
if json.is_a?(Hash)
|
||||
json.any? { |key, value| UNSUPPORTED_JSONLD_KEYWORDS.include?(key) || unsupported_jsonld_features?(value) }
|
||||
elsif json.is_a?(Array)
|
||||
json.any? { |value| unsupported_jsonld_features?(value) }
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# Patches a JSON-LD document to avoid compatibility issues on redistribution
|
||||
#
|
||||
# Since compacting a JSON-LD document against Mastodon's built-in vocabulary
|
||||
|
||||
@@ -223,7 +223,14 @@ module LanguagesHelper
|
||||
'zh-YUE': ['Cantonese', '廣東話'].freeze,
|
||||
}.freeze
|
||||
|
||||
SUPPORTED_LOCALES = {}.merge(ISO_639_1).merge(ISO_639_1_REGIONAL).merge(ISO_639_3).freeze
|
||||
# Since nan is not translated but nan-TW is translated,
|
||||
# to enable the ISO-639-3 language-code with the regional variant but no
|
||||
# official name, we use a specific hash for nan-TW
|
||||
ISO_639_3_REGIONAL = {
|
||||
'nan-TW': ['Hokkien (Taiwan)', '臺語 (Hô-ló話)'].freeze,
|
||||
}.freeze
|
||||
|
||||
SUPPORTED_LOCALES = {}.merge(ISO_639_1).merge(ISO_639_1_REGIONAL).merge(ISO_639_3).merge(ISO_639_3_REGIONAL).freeze
|
||||
|
||||
# For ISO-639-1 and ISO-639-3 language codes, we have their official
|
||||
# names, but for some translations, we need the names of the
|
||||
@@ -233,7 +240,6 @@ module LanguagesHelper
|
||||
'es-AR': 'Español (Argentina)',
|
||||
'es-MX': 'Español (México)',
|
||||
'fr-CA': 'Français (Canadien)',
|
||||
'nan-TW': '臺語 (Hô-ló話)',
|
||||
'pt-BR': 'Português (Brasil)',
|
||||
'pt-PT': 'Português (Portugal)',
|
||||
'sr-Latn': 'Srpski (latinica)',
|
||||
|
||||
6
app/helpers/media_player_helper.rb
Normal file
6
app/helpers/media_player_helper.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module MediaPlayerHelper
|
||||
PLAYER_HEIGHT = 380
|
||||
PLAYER_WIDTH = 670
|
||||
end
|
||||
@@ -23,6 +23,10 @@ module SettingsHelper
|
||||
)
|
||||
end
|
||||
|
||||
def user_settings_collection(value)
|
||||
UserSettings.definition_for(value)&.in || []
|
||||
end
|
||||
|
||||
def author_attribution_name(account)
|
||||
return if account.nil?
|
||||
|
||||
|
||||
@@ -47,9 +47,15 @@ module ThemeHelper
|
||||
end
|
||||
|
||||
def current_theme
|
||||
return Setting.theme unless Themes.instance.names.include? current_user&.setting_theme
|
||||
available_themes = Themes.instance.names
|
||||
|
||||
current_user.setting_theme
|
||||
user_theme = current_user&.setting_theme
|
||||
return user_theme if user_theme && available_themes.include?(user_theme)
|
||||
|
||||
site_theme = Setting.theme
|
||||
return site_theme if available_themes.include?(site_theme)
|
||||
|
||||
'default' # Fallback
|
||||
end
|
||||
|
||||
def color_scheme
|
||||
|
||||
@@ -69,8 +69,9 @@ on('change', '#batch_checkbox_all', ({ target }) => {
|
||||
'.batch-table__select-all',
|
||||
);
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLInputElement>(batchCheckboxClassName)
|
||||
target
|
||||
.closest('.batch-table')
|
||||
?.querySelectorAll<HTMLInputElement>(batchCheckboxClassName)
|
||||
.forEach((content) => {
|
||||
content.checked = target.checked;
|
||||
});
|
||||
@@ -112,17 +113,20 @@ on('click', '.batch-table__select-all button', () => {
|
||||
}
|
||||
});
|
||||
|
||||
on('change', batchCheckboxClassName, () => {
|
||||
const checkAllElement = document.querySelector<HTMLInputElement>(
|
||||
on('change', batchCheckboxClassName, (event) => {
|
||||
const targetTable = (event.target as HTMLElement).closest('.batch-table');
|
||||
if (!targetTable) return;
|
||||
|
||||
const checkAllElement = targetTable.querySelector<HTMLInputElement>(
|
||||
'input#batch_checkbox_all',
|
||||
);
|
||||
const selectAllMatchingElement = document.querySelector(
|
||||
const selectAllMatchingElement = targetTable.querySelector(
|
||||
'.batch-table__select-all',
|
||||
);
|
||||
|
||||
if (checkAllElement) {
|
||||
const allCheckboxes = Array.from(
|
||||
document.querySelectorAll<HTMLInputElement>(batchCheckboxClassName),
|
||||
targetTable.querySelectorAll<HTMLInputElement>(batchCheckboxClassName),
|
||||
);
|
||||
checkAllElement.checked = allCheckboxes.every((content) => content.checked);
|
||||
checkAllElement.indeterminate =
|
||||
|
||||
@@ -13,14 +13,17 @@ import axios from 'axios';
|
||||
import { on } from 'delegated-events';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
import { determineEmojiMode } from '@/mastodon/features/emoji/mode';
|
||||
import { updateHtmlWithEmoji } from '@/mastodon/features/emoji/render';
|
||||
import loadKeyboardExtensions from '@/mastodon/load_keyboard_extensions';
|
||||
import { loadLocale, getLocale } from '@/mastodon/locales';
|
||||
import { loadPolyfills } from '@/mastodon/polyfills';
|
||||
import ready from '@/mastodon/ready';
|
||||
import { assetHost } from '@/mastodon/utils/config';
|
||||
import { getNestedProperty } from '@/mastodon/utils/objects';
|
||||
import { isDarkMode } from '@/mastodon/utils/theme';
|
||||
import { formatTime } from '@/mastodon/utils/time';
|
||||
|
||||
import emojify from '../mastodon/features/emoji/emoji';
|
||||
import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions';
|
||||
import { loadLocale, getLocale } from '../mastodon/locales';
|
||||
import { loadPolyfills } from '../mastodon/polyfills';
|
||||
import ready from '../mastodon/ready';
|
||||
|
||||
import 'cocoon-js-vanilla';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -38,7 +41,7 @@ const messages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
function loaded() {
|
||||
async function loaded() {
|
||||
const { messages: localeData } = getLocale();
|
||||
|
||||
const locale = document.documentElement.lang;
|
||||
@@ -75,9 +78,30 @@ function loaded() {
|
||||
return messageFormat.format(values) as string;
|
||||
};
|
||||
|
||||
document.querySelectorAll('.emojify').forEach((content) => {
|
||||
content.innerHTML = emojify(content.innerHTML);
|
||||
});
|
||||
let emojiStyle = 'auto';
|
||||
const initialStateText =
|
||||
document.getElementById('initial-state')?.textContent;
|
||||
if (initialStateText) {
|
||||
const stateEmojiStyle = getNestedProperty(
|
||||
JSON.parse(initialStateText) as unknown,
|
||||
'meta',
|
||||
'emoji_style',
|
||||
);
|
||||
if (typeof stateEmojiStyle === 'string') {
|
||||
emojiStyle = stateEmojiStyle;
|
||||
}
|
||||
}
|
||||
const emojiMode = determineEmojiMode(emojiStyle);
|
||||
const darkTheme = isDarkMode();
|
||||
for (const element of document.querySelectorAll('.emojify')) {
|
||||
await updateHtmlWithEmoji({
|
||||
assetHost,
|
||||
element,
|
||||
locale,
|
||||
mode: emojiMode,
|
||||
darkTheme,
|
||||
});
|
||||
}
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLTimeElement>('time.formatted')
|
||||
|
||||
1
app/javascript/images/anniversary.svg
Normal file
1
app/javascript/images/anniversary.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.5 KiB |
95
app/javascript/images/elephant_ui.svg
Normal file
95
app/javascript/images/elephant_ui.svg
Normal file
@@ -0,0 +1,95 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 334 353">
|
||||
<ellipse cx="162.778" cy="327.222" fill="var(--color-shadow, #f2f2f7)" rx="113.889" ry="25" />
|
||||
<g clip-path="url(#a)">
|
||||
<path fill="var(--color-skin-1, #d7d6e1)"
|
||||
d="M181.219 292.602s.546 21.402 1.091 27.288 1.636 8.561 10.909 9.096 22.365-1.071 27.274-2.141 8.727-3.21 7.636-15.516-2.182-27.288-2.182-27.288" />
|
||||
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
|
||||
stroke-width="1.826"
|
||||
d="M181.219 292.602s.546 21.402 1.091 27.288 1.636 8.561 10.909 9.096 22.365-1.071 27.274-2.141 8.727-3.21 7.636-15.516-2.182-27.288-2.182-27.288" />
|
||||
<path fill="var(--color-skin-2, #f6f6f9)" stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-miterlimit="10" stroke-width="1.826"
|
||||
d="m227.584 320.851-.054-.321c-.818-3.05-2.455-3.746-4.364-3.371-2.618.535-3.327 4.12-3.764 8.935 0 .214-.054.428-.054.696l.382.16c.218-.053.49-.107.709-.16 3.218-.642 5.945-1.819 7.145-5.939" />
|
||||
<path fill="var(--color-skin-3, #eeedf3)"
|
||||
d="M231.184 121.917c25.092-2.676 43.092-14.982 45.82-17.122 0 0-7.637-6.42-1.637-10.701 6-4.28 9.819 1.07 9.819 1.07s12.818-15.999 23.728-7.438c10.909 8.561 15 17.604 11.182 26.165-3.819 8.561-28.365 38.524-80.184 45.48" />
|
||||
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
|
||||
stroke-width="1.826"
|
||||
d="M231.184 121.917c25.092-2.676 43.092-14.982 45.82-17.122 0 0-7.637-6.42-1.637-10.701 6-4.28 9.819 1.07 9.819 1.07s12.818-15.999 23.728-7.438c10.909 8.561 15 17.604 11.182 26.165-3.819 8.561-28.365 38.524-80.184 45.48" />
|
||||
<path fill="var(--color-skin-1, #d7d6e1)"
|
||||
d="M83.363 281.364c-8.728 8.026-22.364 12.307-29.455 2.676-7.092-9.632-2.182-13.912-3.819-16.052-1.636-2.14-11.454 4.28-7.09 16.587 4.363 12.306 29.455 25.682 52.91 7.49" />
|
||||
<path fill="var(--color-skin-3, #eeedf3)"
|
||||
d="M225.403 101.638s18.873 37.935 23.51 93.849c3.927 47.62 8.182 74.373-25.092 89.89s-77.456 12.841-98.184 13.376-42.546-2.14-48.001-25.148.273-40.932.273-40.932.763-38.096-1.146-44.249c-1.91-6.153-5.018-15.356-5.018-15.356s-3.11 21.456-19.364 28.679c-16.255 7.277-30.601.428-29.401-19.53 0 0-13.91-22.793-20.51-42.751s10.8-39.54 42.71-31.461c1.091-37.99 34.692-69.932 81.33-73.678 46.583-3.799 57.983 20.707 82.856 21.777 24.001 1.07 40.91-6.42 53.456-18.727 12.546-12.306 38.183-28.358 58.365-15.517 0 0 9.818-3.21 10.909 5.351s-3.818 8.56-6.545 10.166c0 0 3.818 11.771-3.819 13.377-7.636 1.605-6.545-9.631-6.545-9.631s-12.546-1.606-22.91 14.446-25.746 37.561-66.874 46.069c-8.782.749-16.091-2.087-16.091-2.087z" />
|
||||
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
|
||||
stroke-width="1.826"
|
||||
d="M225.403 101.638s18.873 37.935 23.51 93.849c3.927 47.62 8.182 74.373-25.092 89.89s-77.456 12.841-98.184 13.376-42.546-2.14-48.001-25.148.273-40.932.273-40.932.763-38.096-1.146-44.249c-1.91-6.153-5.018-15.356-5.018-15.356s-3.11 21.456-19.364 28.679c-16.255 7.277-30.601.428-29.401-19.53 0 0-13.91-22.793-20.51-42.751s10.8-39.54 42.71-31.461c1.091-37.99 34.692-69.932 81.33-73.678 46.583-3.799 57.983 20.707 82.856 21.777 24.001 1.07 40.91-6.42 53.456-18.727 12.546-12.306 38.183-28.358 58.365-15.517 0 0 9.818-3.21 10.909 5.351s-3.818 8.56-6.545 10.166c0 0 3.818 11.771-3.819 13.377-7.636 1.605-6.545-9.631-6.545-9.631s-12.546-1.606-22.91 14.446-25.746 37.561-66.874 46.069m0 0c-8.782.749-16.091-2.087-16.091-2.087" />
|
||||
<path fill="var(--color-skin-1, #d7d6e1)"
|
||||
d="M94.272 285.11c0 12.306-2.182 28.358-2.727 34.244-.546 5.885 1.636 11.985 11.454 12.306 15.273.535 44.729 2.675 47.456-6.421 3.436-11.557 2.727-31.033 0-37.454" />
|
||||
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
|
||||
stroke-width="1.826"
|
||||
d="M94.272 285.11c0 12.306-2.182 28.358-2.727 34.244-.546 5.885 1.636 11.985 11.454 12.306 15.273.535 44.729 2.675 47.456-6.421 3.436-11.557 2.727-31.033 0-37.454" />
|
||||
<path fill="var(--color-skin-2, #f6f6f9)" d="M113.471 332.035c.437-5.19.982-8.401 3.71-8.936s4.8 1.659 4.854 9.15" />
|
||||
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
|
||||
stroke-width="1.826" d="M113.471 332.035c.437-5.19.982-8.401 3.71-8.936s4.8 1.659 4.854 9.15" />
|
||||
<path fill="var(--color-skin-2, #f6f6f9)" d="M122.199 332.035c.437-5.19.982-8.401 3.709-8.936s4.801 1.659 4.855 9.15" />
|
||||
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
|
||||
stroke-width="1.826"
|
||||
d="M122.199 332.035c.437-5.19.982-8.401 3.709-8.936s4.801 1.659 4.855 9.15" />
|
||||
<path fill="var(--color-skin-2, #f6f6f9)"
|
||||
d="M130.818 332.195c.437-5.19 1.091-9.096 3.818-9.631s4.855 1.124 4.91 8.561" />
|
||||
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
|
||||
stroke-width="1.826"
|
||||
d="M130.818 332.195c.437-5.19 1.091-9.096 3.818-9.631s4.855 1.124 4.91 8.561" />
|
||||
<path fill="var(--color-skin-1, #d7d6e1)"
|
||||
d="M21.835 140.216c3.982 4.869 11.237 20.439 10.8 32.639-.49 12.252 12.982 17.549 20.073 6.955s9.055-31.247 4.146-41.306c-4.91-10.113-7.582-13.912-7.582-13.912s-2.673 4.816-9.546 5.779-7.09-1.659-9.436-5.244c0 0 .327 3.96-5.782 6.153-6.11 2.248-8.073-2.675-8.073-2.675s1.473 6.795 5.4 11.611m99.438 157.521c-11.619 0-20.074-1.444-26.674-4.494-8.4-3.906-13.636-10.38-15.873-19.904-5.345-22.473.218-40.183.273-40.344l.055-.16v-.161s.054-1.979.054-5.19c7.582.268 18.164 1.498 27.437 5.779 9.164 4.227 14.946 10.487 17.237 18.513 5.073 17.871 18.273 26.539 40.419 26.539 6.873 0 14.728-.803 23.946-2.461 4.691-.857 9.327-1.499 13.8-2.141 21.11-2.996 39.71-5.618 47.947-25.629-1.8 15.463-8.073 27.662-26.51 36.277-27.982 13.055-63.383 13.109-86.784 13.162-4.2 0-7.8 0-10.964.107-1.582.054-3 .107-4.363.107" />
|
||||
<path fill="var(--color-outline, #b2b1c8)" stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-miterlimit="10" stroke-width="1.826"
|
||||
d="M180.456 116.78c.709-4.548 1.091-7.865-4.255-6.741-5.345 1.123-38.073 9.631-43.91 10.861-5.891 1.231-4.745 6.475.982 9.631s21.655 12.414 28.201 11.558 12.382-2.034 15.054-10.273c2.673-8.187 3.928-15.036 3.928-15.036" />
|
||||
<path fill="var(--color-skin-1, #d7d6e1)"
|
||||
d="M160.6 141.072c-6.218 0-19.8-7.597-25.582-10.808l-1.2-.695c-2.782-1.552-4.582-3.799-4.309-5.458.163-1.07 1.254-1.819 3-2.194 3-.642 13.091-3.157 22.8-5.618l2.073-.535c8.673 2.247 15.491 7.973 18.219 15.356l-.11.375c-2.454 7.491-7.418 8.668-14.127 9.524-.273.053-.491.053-.764.053" />
|
||||
<path fill="var(--color-highlight, #fff)"
|
||||
d="M130.817 128.925c-1.963-1.391-3.054-3.371-2.727-5.083.273-1.498 1.473-2.515 3.436-2.889 3.164-.643 13.692-3.318 23.837-5.886 4.855-1.231 9.546-2.408 13.255-3.317l1.855 8.025z" />
|
||||
<path fill="var(--color-outline, #b2b1c8)"
|
||||
d="m167.855 113.087 1.364 5.939-38.183 8.775c-1.363-1.123-2.127-2.514-1.909-3.692.273-1.391 1.746-1.872 2.618-2.086 3.164-.642 13.692-3.318 23.892-5.886 4.473-1.124 8.727-2.194 12.218-3.05m1.637-2.622c-11.128 2.729-33.383 8.454-38.183 9.471-5.618 1.177-5.782 6.956-.709 10.166l41.237-9.524z" />
|
||||
<path fill="var(--color-skin-3, #eeedf3)"
|
||||
d="M70.163 79.22c-11.292 1.819-20.128-.16-23.837-14.66-3.71-14.501 9.273-31.516 27.928-38.15s37.091-1.231 37.091-1.231 5.182-12.039 6.982-18.995 7.255-7.544 12.873 2.783c0 0 15.71-2.194 17.619 14.34 1.964 16.532-11.291 24.826-11.291 24.826" />
|
||||
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
|
||||
stroke-width="1.826"
|
||||
d="M70.163 79.22c-11.292 1.819-20.128-.16-23.837-14.66-3.71-14.501 9.273-31.516 27.928-38.15s37.091-1.231 37.091-1.231 5.182-12.039 6.982-18.995 7.255-7.544 12.873 2.783c0 0 15.71-2.194 17.619 14.34 1.964 16.532-11.291 24.826-11.291 24.826" />
|
||||
<path fill="var(--color-skin-2, #f6f6f9)" d="M131.255 8.912c2.345 5.511 1.582 12.253 1.473 14.447z" />
|
||||
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
|
||||
stroke-width="1.826" d="M131.255 8.912c2.345 5.511 1.582 12.253 1.473 14.447" />
|
||||
<path fill="var(--color-outline, #b2b1c8)"
|
||||
d="M106.055 118.172c1.2 3.049 3.382 5.297 4.854 4.441 1.473-.857 1.582-3.05.219-6.635-1.309-3.532-7.419-15.945-9.055-18.353-1.637-2.407-3.491-3.103-4.964-2.247s-1.036 2.943-.054 4.923c1.036 2.033 9 17.871 9 17.871" />
|
||||
<path fill="var(--color-highlight, #fff)"
|
||||
d="M121.709 168.307c-2.345 0-4.8-.267-7.2-.856-11.564-2.782-17.946-11.504-19.91-18.192-1.254-4.28-.872-7.972 1.091-9.898 1.746-1.766 3.655-2.676 5.564-2.676 4.091 0 7.855 3.853 9.709 6.153 4.146 5.083 8.073 7.652 11.673 7.652 2.891 0 5.4-1.605 7.473-4.816 5.455-8.24 6.928-18.245 4.473-29.642-.818-3.799.164-5.993 1.146-7.17a5.1 5.1 0 0 1 3.981-1.926q1.31 0 2.291.642c9.928 6.314 11.019 27.823 7.419 39.808-3.055 10.06-13.037 20.921-27.71 20.921" />
|
||||
<path fill="var(--color-outline, #b2b1c8)"
|
||||
d="M139.654 108.005c.655 0 1.2.16 1.691.481 4.146 2.622 7.091 8.561 8.292 16.694 1.09 7.277.545 15.677-1.364 21.938-1.582 5.297-4.964 10.273-9.437 13.911-4.963 4.066-10.909 6.207-17.182 6.207a28.3 28.3 0 0 1-6.927-.856c-11.128-2.676-17.237-11.076-19.092-17.443-1.145-3.853-.818-7.224.819-8.829 1.581-1.551 3.163-2.354 4.8-2.354 3.763 0 7.472 4.013 8.891 5.725 4.363 5.351 8.564 8.026 12.491 8.026 3.273 0 6.109-1.766 8.4-5.297 5.618-8.508 7.146-18.727 4.582-30.445-.6-2.622-.273-4.869.927-6.26.818-.963 1.964-1.498 3.109-1.498m0-2.141c-3.654 0-7.691 3.425-6.163 10.381 1.963 8.882 1.963 19.262-4.31 28.786-2.072 3.157-4.363 4.334-6.6 4.334-4.472 0-8.782-4.709-10.8-7.224-2.073-2.568-6.055-6.527-10.582-6.527-2.073 0-4.2.802-6.382 2.996-6 6.046 1.091 25.469 19.364 29.856 2.564.642 5.073.91 7.473.91 14.564 0 25.419-10.594 28.746-21.67 3.873-12.841 2.509-34.458-7.855-41.039-.818-.535-1.854-.803-2.891-.803" />
|
||||
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
|
||||
stroke-width="1.826"
|
||||
d="M110.8 152.737c3.545.909 10.582 1.07 16.255-3.532m-79.965-22.42c-2.018 2.675-5.51 4.227-8.891 3.853-3.382-.321-6.491-2.515-7.91-5.565-.6 2.889-2.89 5.458-5.781 6.153-2.891.696-6.219-.374-8.073-2.675.873 6.956 6.545 12.146 9 18.727" />
|
||||
<path fill="var(--color-skin-1, #d7d6e1)"
|
||||
d="m96.181 132.778-9.327 3.478c-2.728 1.017-5.782-.321-6.764-2.996a5.076 5.076 0 0 1 3.054-6.635l9.328-3.478c2.727-1.017 5.782.321 6.764 2.996 1.036 2.622-.328 5.618-3.055 6.635" />
|
||||
<path stroke="var(--color-skin-2, #f6f6f9)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
|
||||
stroke-width="1.826"
|
||||
d="M293.641 24.964c3.982-1.498 7.691-2.3 10.473-2.033M258.73 51.235c6.382-6.1 11.945-11.076 17.236-15.838 2.128-1.873 4.746-3.799 7.582-5.511M209.584 64.88c8.564.748 18.328-.054 28.746-3.532M149.8 42.246c10.855 1.873 19.855 6.528 30.601 12.627" />
|
||||
<path fill="var(--color-skin-3, #eeedf3)"
|
||||
d="M80.09 177.027c-26.183 11.772-37.092 19.798-31.092 41.735s32.182 21.937 32.182 21.937-4.363 13.377 9.819 14.982 30.001-2.675 30.546-21.937c.546-19.262-10.909-32.639-34.364-34.779" />
|
||||
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
|
||||
stroke-width="1.826"
|
||||
d="M80.09 177.027c-26.183 11.772-37.092 19.798-31.092 41.735s32.182 21.937 32.182 21.937-4.363 13.377 9.819 14.982 30.001-2.675 30.546-21.937c.546-19.262-10.909-32.639-34.364-34.779" />
|
||||
<path fill="var(--color-skin-2, #f6f6f9)" d="M81.18 240.699c5.454 1.07 13.636 1.07 13.636 1.07z" />
|
||||
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
|
||||
stroke-width="1.826" d="M81.18 240.699c5.454 1.07 13.636 1.07 13.636 1.07" />
|
||||
<path fill="var(--color-skin-2, #f6f6f9)"
|
||||
d="m103.654 210.041 3.545 3.639c2.673 2.729 2.564 7.116-.218 9.738l-1.036.963c-2.782 2.622-7.255 2.515-9.928-.214l-3.545-3.638c-2.673-2.729-2.564-7.117.218-9.738l1.036-.964c2.782-2.621 7.255-2.514 9.928.214" />
|
||||
<path fill="var(--color-skin-3, #eeedf3)"
|
||||
d="m169.001 136.15-3.546 1.391c-2.672 1.016-5.727-.268-6.763-2.89l-.382-.963c-1.037-2.621.273-5.618 2.945-6.634l3.546-1.392c2.673-1.016 5.727.268 6.764 2.89l.382.963c1.09 2.622-.273 5.618-2.946 6.635" />
|
||||
<path fill="var(--color-skin-2, #f6f6f9)" d="M71.744 173.015c.982-7.491.327-11.825.327-11.825z" />
|
||||
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
|
||||
stroke-width="1.826" d="M71.744 173.015c.982-7.491.327-11.825.327-11.825" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="var(--color-highlight, #fff)" d="M0 0h333.333v333.333H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 11 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 11 KiB |
3
app/javascript/images/icons/icon_clock.svg
Normal file
3
app/javascript/images/icons/icon_clock.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="black">
|
||||
<path d="M13.2502 8.00029C13.2502 5.10089 10.8996 2.75044 8.00024 2.75029C5.10075 2.75029 2.75024 5.10079 2.75024 8.00029C2.7504 10.8997 5.10084 13.2503 8.00024 13.2503C10.8995 13.2501 13.2501 10.8996 13.2502 8.00029ZM7.41663 4.50029C7.41663 4.17812 7.67808 3.91667 8.00024 3.91667C8.32228 3.91683 8.58301 4.17822 8.58301 4.50029V7.63884L10.5945 8.64458C10.8825 8.78865 10.999 9.13922 10.8551 9.42729C10.711 9.71541 10.3605 9.83276 10.0724 9.68877L7.73877 8.52153C7.54143 8.4227 7.41673 8.221 7.41663 8.00029V4.50029ZM14.4166 8.00029C14.4165 11.5439 11.5438 14.4165 8.00024 14.4167C4.45651 14.4167 1.58316 11.544 1.58301 8.00029C1.58301 4.45646 4.45642 1.58305 8.00024 1.58305C11.5439 1.58321 14.4166 4.45656 14.4166 8.00029Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 799 B |
1
app/javascript/images/icons/icon_follower.svg
Normal file
1
app/javascript/images/icons/icon_follower.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><path fill="currentColor" d="M7.467 7.266 11.533 3.2a.586.586 0 0 1 .85-.001.585.585 0 0 1 0 .851L8.317 8.116zm1.416 1.417 3.5-3.5a.586.586 0 0 1 .85 0 .586.586 0 0 1 0 .85l-3.5 3.5zM3.35 12.267Q2 10.917 1.983 9.017q-.015-1.9 1.334-3.25L5.2 3.882l.883.9q.135.133.25.283.117.15.15.35l2.5-2.5a.586.586 0 0 1 .85 0 .586.586 0 0 1 0 .85L6.8 6.8l-.75.766.15.15q.65.65.642 1.55a2.15 2.15 0 0 1-.659 1.55l-.416.417-.85-.85.416-.417a.98.98 0 0 0 .292-.7.94.94 0 0 0-.276-.705l-.582-.578a.586.586 0 0 1 0-.85l.433-.417a.8.8 0 0 0 .233-.57.77.77 0 0 0-.233-.563L4.15 6.616a3.2 3.2 0 0 0-.975 2.409A3.37 3.37 0 0 0 4.2 11.433q1 1 2.387 1t2.38-1L12.68 7.72a.583.583 0 0 1 .853-.002.586.586 0 0 1 0 .85l-3.716 3.7q-1.342 1.35-3.23 1.35t-3.237-1.35M11.2 15.2V14a2.7 2.7 0 0 0 1.983-.817A2.7 2.7 0 0 0 14 11.2h1.2q0 1.66-1.17 2.83T11.2 15.2M.8 4.8q0-1.66 1.17-2.83T4.8.8V2a2.7 2.7 0 0 0-1.983.816A2.7 2.7 0 0 0 2 4.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 984 B |
@@ -6,8 +6,8 @@ import { throttle } from 'lodash';
|
||||
import api from 'mastodon/api';
|
||||
import { browserHistory } from 'mastodon/components/router';
|
||||
import { countableText } from 'mastodon/features/compose/util/counter';
|
||||
import { search as emojiSearch } from 'mastodon/features/emoji/emoji_mart_search_light';
|
||||
import { tagHistory } from 'mastodon/settings';
|
||||
import { emojiMartSearch } from '@/mastodon/features/emoji/picker';
|
||||
|
||||
import { showAlert, showAlertForError } from './alerts';
|
||||
import { useEmoji } from './emojis';
|
||||
@@ -99,7 +99,7 @@ export const ensureComposeIsVisible = (getState) => {
|
||||
|
||||
export function setComposeToStatus(status, text, spoiler_text) {
|
||||
return (dispatch, getState) => {
|
||||
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
|
||||
const maxOptions = getState().server.server.item?.configuration.polls.max_options;
|
||||
|
||||
dispatch({
|
||||
type: COMPOSE_SET_STATUS,
|
||||
@@ -153,10 +153,11 @@ export function resetCompose() {
|
||||
};
|
||||
}
|
||||
|
||||
export const focusCompose = (defaultText = '') => (dispatch, getState) => {
|
||||
export const focusCompose = (defaultText = '', caretStart = false) => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: COMPOSE_FOCUS,
|
||||
defaultText,
|
||||
caretStart,
|
||||
});
|
||||
|
||||
ensureComposeIsVisible(getState);
|
||||
@@ -562,7 +563,7 @@ export function clearComposeSuggestions() {
|
||||
};
|
||||
}
|
||||
|
||||
const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => {
|
||||
const fetchComposeSuggestionsAccounts = throttle((dispatch, token) => {
|
||||
if (fetchComposeSuggestionsAccountsController) {
|
||||
fetchComposeSuggestionsAccountsController.abort();
|
||||
}
|
||||
@@ -589,12 +590,14 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) =>
|
||||
});
|
||||
}, 200, { leading: true, trailing: true });
|
||||
|
||||
const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
|
||||
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 });
|
||||
const fetchComposeSuggestionsEmojis = async (dispatch, token) => {
|
||||
// Right now we are hard-coding the locale to English since the picker search only supports English.
|
||||
// Once we replace the legacy picker we can remove this and use the actual locale of the user.
|
||||
const results = await emojiMartSearch(token, 'en', 5);
|
||||
dispatch(readyComposeSuggestionsEmojis(token, results));
|
||||
};
|
||||
|
||||
const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
|
||||
const fetchComposeSuggestionsTags = throttle((dispatch, token) => {
|
||||
if (fetchComposeSuggestionsTagsController) {
|
||||
fetchComposeSuggestionsTagsController.abort();
|
||||
}
|
||||
@@ -628,14 +631,14 @@ export function fetchComposeSuggestions(token) {
|
||||
return (dispatch, getState) => {
|
||||
switch (token[0]) {
|
||||
case ':':
|
||||
fetchComposeSuggestionsEmojis(dispatch, getState, token);
|
||||
void fetchComposeSuggestionsEmojis(dispatch, token);
|
||||
break;
|
||||
case '#':
|
||||
case '#':
|
||||
fetchComposeSuggestionsTags(dispatch, getState, token);
|
||||
fetchComposeSuggestionsTags(dispatch, token);
|
||||
break;
|
||||
default:
|
||||
fetchComposeSuggestionsAccounts(dispatch, getState, token);
|
||||
fetchComposeSuggestionsAccounts(dispatch, token);
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -668,7 +671,7 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
|
||||
let completion, startPosition;
|
||||
|
||||
if (suggestion.type === 'emoji') {
|
||||
completion = suggestion.native || suggestion.colons;
|
||||
completion = suggestion.native || `:${suggestion.id}:`;
|
||||
startPosition = position - 1;
|
||||
|
||||
dispatch(useEmoji(suggestion));
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import api from '../api';
|
||||
|
||||
export const CUSTOM_EMOJIS_FETCH_REQUEST = 'CUSTOM_EMOJIS_FETCH_REQUEST';
|
||||
export const CUSTOM_EMOJIS_FETCH_SUCCESS = 'CUSTOM_EMOJIS_FETCH_SUCCESS';
|
||||
export const CUSTOM_EMOJIS_FETCH_FAIL = 'CUSTOM_EMOJIS_FETCH_FAIL';
|
||||
|
||||
export function fetchCustomEmojis() {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchCustomEmojisRequest());
|
||||
|
||||
api().get('/api/v1/custom_emojis').then(response => {
|
||||
dispatch(fetchCustomEmojisSuccess(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchCustomEmojisFail(error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCustomEmojisRequest() {
|
||||
return {
|
||||
type: CUSTOM_EMOJIS_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCustomEmojisSuccess(custom_emojis) {
|
||||
return {
|
||||
type: CUSTOM_EMOJIS_FETCH_SUCCESS,
|
||||
custom_emojis,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCustomEmojisFail(error) {
|
||||
return {
|
||||
type: CUSTOM_EMOJIS_FETCH_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import api from '../api';
|
||||
|
||||
import { importFetchedAccount } from './importer';
|
||||
|
||||
export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST';
|
||||
export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS';
|
||||
export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL';
|
||||
|
||||
export const SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST = 'SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST';
|
||||
export const SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS = 'SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS';
|
||||
export const SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL = 'SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL';
|
||||
|
||||
export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST';
|
||||
export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS';
|
||||
export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL';
|
||||
|
||||
export const SERVER_DOMAIN_BLOCKS_FETCH_REQUEST = 'SERVER_DOMAIN_BLOCKS_FETCH_REQUEST';
|
||||
export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS';
|
||||
export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL';
|
||||
|
||||
export const fetchServer = () => (dispatch, getState) => {
|
||||
if (getState().getIn(['server', 'server', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchServerRequest());
|
||||
|
||||
api()
|
||||
.get('/api/v2/instance').then(({ data }) => {
|
||||
// Only import the account if it doesn't already exist,
|
||||
// because the API is cached even for logged in users.
|
||||
const account = data.contact.account;
|
||||
if (account) {
|
||||
const existingAccount = getState().getIn(['accounts', account.id]);
|
||||
if (!existingAccount) {
|
||||
dispatch(importFetchedAccount(account));
|
||||
}
|
||||
}
|
||||
dispatch(fetchServerSuccess(data));
|
||||
}).catch(err => dispatch(fetchServerFail(err)));
|
||||
};
|
||||
|
||||
const fetchServerRequest = () => ({
|
||||
type: SERVER_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchServerSuccess = server => ({
|
||||
type: SERVER_FETCH_SUCCESS,
|
||||
server,
|
||||
});
|
||||
|
||||
const fetchServerFail = error => ({
|
||||
type: SERVER_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchServerTranslationLanguages = () => (dispatch) => {
|
||||
dispatch(fetchServerTranslationLanguagesRequest());
|
||||
|
||||
api()
|
||||
.get('/api/v1/instance/translation_languages').then(({ data }) => {
|
||||
dispatch(fetchServerTranslationLanguagesSuccess(data));
|
||||
}).catch(err => dispatch(fetchServerTranslationLanguagesFail(err)));
|
||||
};
|
||||
|
||||
const fetchServerTranslationLanguagesRequest = () => ({
|
||||
type: SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchServerTranslationLanguagesSuccess = translationLanguages => ({
|
||||
type: SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS,
|
||||
translationLanguages,
|
||||
});
|
||||
|
||||
const fetchServerTranslationLanguagesFail = error => ({
|
||||
type: SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchExtendedDescription = () => (dispatch, getState) => {
|
||||
if (getState().getIn(['server', 'extendedDescription', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchExtendedDescriptionRequest());
|
||||
|
||||
api()
|
||||
.get('/api/v1/instance/extended_description')
|
||||
.then(({ data }) => dispatch(fetchExtendedDescriptionSuccess(data)))
|
||||
.catch(err => dispatch(fetchExtendedDescriptionFail(err)));
|
||||
};
|
||||
|
||||
const fetchExtendedDescriptionRequest = () => ({
|
||||
type: EXTENDED_DESCRIPTION_REQUEST,
|
||||
});
|
||||
|
||||
const fetchExtendedDescriptionSuccess = description => ({
|
||||
type: EXTENDED_DESCRIPTION_SUCCESS,
|
||||
description,
|
||||
});
|
||||
|
||||
const fetchExtendedDescriptionFail = error => ({
|
||||
type: EXTENDED_DESCRIPTION_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchDomainBlocks = () => (dispatch, getState) => {
|
||||
if (getState().getIn(['server', 'domainBlocks', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchDomainBlocksRequest());
|
||||
|
||||
api()
|
||||
.get('/api/v1/instance/domain_blocks')
|
||||
.then(({ data }) => dispatch(fetchDomainBlocksSuccess(true, data)))
|
||||
.catch(err => {
|
||||
if (err.response.status === 404) {
|
||||
dispatch(fetchDomainBlocksSuccess(false, []));
|
||||
} else {
|
||||
dispatch(fetchDomainBlocksFail(err));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const fetchDomainBlocksRequest = () => ({
|
||||
type: SERVER_DOMAIN_BLOCKS_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchDomainBlocksSuccess = (isAvailable, blocks) => ({
|
||||
type: SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS,
|
||||
isAvailable,
|
||||
blocks,
|
||||
});
|
||||
|
||||
const fetchDomainBlocksFail = error => ({
|
||||
type: SERVER_DOMAIN_BLOCKS_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
34
app/javascript/mastodon/actions/server.ts
Normal file
34
app/javascript/mastodon/actions/server.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
apiGetInstance,
|
||||
apiGetExtendedDescription,
|
||||
apiGetDomainBlocks,
|
||||
apiGetTranslationLanguages,
|
||||
} from 'mastodon/api/instance';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
import { importFetchedAccount } from './importer';
|
||||
|
||||
export const fetchServer = createDataLoadingThunk(
|
||||
'server/fetch',
|
||||
() => apiGetInstance(),
|
||||
(instance, { dispatch }) => {
|
||||
if (instance.contact.account) {
|
||||
dispatch(importFetchedAccount(instance.contact.account));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const fetchExtendedDescription = createDataLoadingThunk(
|
||||
'server/extended_description',
|
||||
() => apiGetExtendedDescription(),
|
||||
);
|
||||
|
||||
export const fetchServerTranslationLanguages = createDataLoadingThunk(
|
||||
'server/translation_languages',
|
||||
() => apiGetTranslationLanguages(),
|
||||
);
|
||||
|
||||
export const fetchDomainBlocks = createDataLoadingThunk(
|
||||
'server/domain_blocks',
|
||||
() => apiGetDomainBlocks(),
|
||||
);
|
||||
@@ -111,7 +111,7 @@ export function fetchStatusFail(id, error, skipLoading, parentQuotePostId) {
|
||||
|
||||
export function redraft(status, raw_text, quoted_status_id = null) {
|
||||
return (dispatch, getState) => {
|
||||
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
|
||||
const maxOptions = getState().server.server.item?.configuration.polls.max_options;
|
||||
|
||||
dispatch({
|
||||
type: REDRAFT,
|
||||
|
||||
@@ -34,11 +34,16 @@ export const apiGetCollection = (collectionId: string) =>
|
||||
`v1_alpha/collections/${collectionId}`,
|
||||
);
|
||||
|
||||
export const apiGetAccountCollections = (accountId: string) =>
|
||||
export const apiGetCollectionsCreatedByAccount = (accountId: string) =>
|
||||
apiRequestGet<ApiCollectionsJSON>(
|
||||
`v1_alpha/accounts/${accountId}/collections`,
|
||||
);
|
||||
|
||||
export const apiGetCollectionsFeaturingAccount = (accountId: string) =>
|
||||
apiRequestGet<ApiCollectionsJSON>(
|
||||
`v1_alpha/accounts/${accountId}/in_collections`,
|
||||
);
|
||||
|
||||
export const apiAddCollectionItem = (collectionId: string, accountId: string) =>
|
||||
apiRequestPost<WrappedCollectionAccountItem>(
|
||||
`v1_alpha/collections/${collectionId}/items`,
|
||||
|
||||
@@ -2,6 +2,10 @@ import { apiRequestGet } from 'mastodon/api';
|
||||
import type {
|
||||
ApiTermsOfServiceJSON,
|
||||
ApiPrivacyPolicyJSON,
|
||||
ApiInstanceJSON,
|
||||
ApiExtendedDescriptionJSON,
|
||||
ApiTranslationLanguagesJSON,
|
||||
ApiDomainBlockJSON,
|
||||
} from 'mastodon/api_types/instance';
|
||||
|
||||
export const apiGetTermsOfService = (version?: string) =>
|
||||
@@ -13,3 +17,17 @@ export const apiGetTermsOfService = (version?: string) =>
|
||||
|
||||
export const apiGetPrivacyPolicy = () =>
|
||||
apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');
|
||||
|
||||
export const apiGetInstance = () =>
|
||||
apiRequestGet<ApiInstanceJSON>('v2/instance');
|
||||
|
||||
export const apiGetExtendedDescription = () =>
|
||||
apiRequestGet<ApiExtendedDescriptionJSON>('v1/instance/extended_description');
|
||||
|
||||
export const apiGetTranslationLanguages = () =>
|
||||
apiRequestGet<ApiTranslationLanguagesJSON>(
|
||||
'v1/instance/translation_languages',
|
||||
);
|
||||
|
||||
export const apiGetDomainBlocks = () =>
|
||||
apiRequestGet<ApiDomainBlockJSON[]>('v1/instance/domain_blocks');
|
||||
|
||||
@@ -53,7 +53,7 @@ export interface BaseApiAccountJSON {
|
||||
header_static: string;
|
||||
header_description: string;
|
||||
id: string;
|
||||
last_status_at: string;
|
||||
last_status_at: string | null;
|
||||
locked: boolean;
|
||||
show_media: boolean;
|
||||
show_media_replies: boolean;
|
||||
|
||||
@@ -11,7 +11,8 @@ export interface ApiCollectionJSON {
|
||||
account_id: string;
|
||||
|
||||
id: string;
|
||||
uri: string | null;
|
||||
uri: string;
|
||||
url: string;
|
||||
local: boolean;
|
||||
item_count: number;
|
||||
|
||||
@@ -56,7 +57,7 @@ export interface CollectionAccountItem {
|
||||
id: string;
|
||||
account_id?: string; // Only present when state is 'accepted' (or the collection is your own)
|
||||
state: 'pending' | 'accepted' | 'rejected' | 'revoked';
|
||||
position: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface WrappedCollectionAccountItem {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ApiAccountJSON } from './accounts';
|
||||
|
||||
export interface ApiTermsOfServiceJSON {
|
||||
effective_date: string;
|
||||
effective: boolean;
|
||||
@@ -9,3 +11,136 @@ export interface ApiPrivacyPolicyJSON {
|
||||
updated_at: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface ApiBaseRuleJSON {
|
||||
text: string;
|
||||
hint: string;
|
||||
}
|
||||
|
||||
export interface ApiRuleJSON {
|
||||
id: string;
|
||||
text: string;
|
||||
hint: string;
|
||||
translations?: Record<string, ApiBaseRuleJSON>;
|
||||
}
|
||||
|
||||
export interface ApiExtendedDescriptionJSON {
|
||||
updated_at: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ApiDomainBlockJSON {
|
||||
domain: string;
|
||||
digest: string;
|
||||
severity: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
export type ApiTranslationLanguagesJSON = Record<string, string[]>;
|
||||
|
||||
export interface ApiInstanceJSON {
|
||||
domain: string;
|
||||
title: string;
|
||||
version: string;
|
||||
source_url: string;
|
||||
description: string;
|
||||
languages: string[];
|
||||
usage: {
|
||||
users: {
|
||||
active_month: number;
|
||||
};
|
||||
};
|
||||
thumbnail: {
|
||||
url: string;
|
||||
blurhash?: string;
|
||||
description: string;
|
||||
versions?: Record<string, string>;
|
||||
};
|
||||
contact: {
|
||||
email: string | null;
|
||||
account: ApiAccountJSON | null;
|
||||
};
|
||||
api_versions: {
|
||||
mastodon: number;
|
||||
};
|
||||
registrations: {
|
||||
enabled: boolean;
|
||||
approval_required: boolean;
|
||||
reason_required: boolean | null;
|
||||
message: string | null;
|
||||
min_age: string | null;
|
||||
url: string | null;
|
||||
};
|
||||
rules: ApiRuleJSON[];
|
||||
configuration: {
|
||||
urls: {
|
||||
streaming: string;
|
||||
status: string | null;
|
||||
about: string;
|
||||
privacy_policy: string | null;
|
||||
terms_of_service: string | null;
|
||||
};
|
||||
|
||||
vapid: {
|
||||
public_key: string;
|
||||
};
|
||||
|
||||
accounts: {
|
||||
max_display_name_length: number;
|
||||
max_note_length: number;
|
||||
max_avatar_description_length: number;
|
||||
max_header_description_length: number;
|
||||
max_featured_tags: number;
|
||||
max_pinned_statuses: number;
|
||||
max_profile_fields: number;
|
||||
profile_field_name_limit: number;
|
||||
profile_field_value_limit: number;
|
||||
};
|
||||
|
||||
statuses: {
|
||||
max_characters: number;
|
||||
max_media_attachments: number;
|
||||
characters_reserved_per_url: number;
|
||||
};
|
||||
|
||||
media_attachments: {
|
||||
description_limit: number;
|
||||
image_matrix_limit: number;
|
||||
image_size_limit: number;
|
||||
supported_mime_types: string[];
|
||||
video_frame_rate_limit: number;
|
||||
video_matrix_limit: number;
|
||||
video_size_limit: number;
|
||||
};
|
||||
|
||||
polls: {
|
||||
max_options: number;
|
||||
max_characters_per_option: number;
|
||||
min_expiration: number;
|
||||
max_expiration: number;
|
||||
};
|
||||
|
||||
translation: {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
timeline_access: {
|
||||
live_feeds: {
|
||||
local: string;
|
||||
remote: string;
|
||||
};
|
||||
|
||||
hashtag_feeds: {
|
||||
local: string;
|
||||
remote: string;
|
||||
};
|
||||
|
||||
trending_link_feeds: {
|
||||
local: string;
|
||||
remote: string;
|
||||
};
|
||||
};
|
||||
|
||||
limited_federation: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { AccountWarningAction } from 'mastodon/models/notification_group';
|
||||
|
||||
import type { ApiAccountJSON } from './accounts';
|
||||
import type { ApiCollectionJSON } from './collections';
|
||||
import type { ApiReportJSON } from './reports';
|
||||
import type { ApiStatusJSON } from './statuses';
|
||||
|
||||
@@ -22,6 +23,8 @@ export const allNotificationTypes: NotificationType[] = [
|
||||
'moderation_warning',
|
||||
'severed_relationships',
|
||||
'annual_report',
|
||||
'added_to_collection',
|
||||
'collection_update',
|
||||
];
|
||||
|
||||
export type NotificationWithStatusType =
|
||||
@@ -42,7 +45,9 @@ export type NotificationType =
|
||||
| 'severed_relationships'
|
||||
| 'admin.sign_up'
|
||||
| 'admin.report'
|
||||
| 'annual_report';
|
||||
| 'annual_report'
|
||||
| 'added_to_collection'
|
||||
| 'collection_update';
|
||||
|
||||
export interface BaseNotificationJSON {
|
||||
id: string;
|
||||
@@ -83,6 +88,26 @@ interface ReportNotificationJSON extends BaseNotificationJSON {
|
||||
report: ApiReportJSON;
|
||||
}
|
||||
|
||||
interface AddedToCollectionNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||
type: 'added_to_collection';
|
||||
collection: ApiCollectionJSON;
|
||||
}
|
||||
|
||||
interface AddedToCollectionNotificationJSON extends BaseNotificationJSON {
|
||||
type: 'added_to_collection';
|
||||
collection: ApiCollectionJSON;
|
||||
}
|
||||
|
||||
interface CollectionUpdateNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||
type: 'collection_update';
|
||||
collection: ApiCollectionJSON;
|
||||
}
|
||||
|
||||
interface CollectionUpdateNotificationJSON extends BaseNotificationJSON {
|
||||
type: 'collection_update';
|
||||
collection: ApiCollectionJSON;
|
||||
}
|
||||
|
||||
type SimpleNotificationTypes = 'follow' | 'follow_request' | 'admin.sign_up';
|
||||
interface SimpleNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||
type: SimpleNotificationTypes;
|
||||
@@ -146,7 +171,9 @@ export type ApiNotificationJSON =
|
||||
| ReportNotificationJSON
|
||||
| AccountRelationshipSeveranceNotificationJSON
|
||||
| NotificationWithStatusJSON
|
||||
| ModerationWarningNotificationJSON;
|
||||
| ModerationWarningNotificationJSON
|
||||
| AddedToCollectionNotificationJSON
|
||||
| CollectionUpdateNotificationJSON;
|
||||
|
||||
export type ApiNotificationGroupJSON =
|
||||
| SimpleNotificationGroupJSON
|
||||
@@ -154,7 +181,9 @@ export type ApiNotificationGroupJSON =
|
||||
| AccountRelationshipSeveranceNotificationGroupJSON
|
||||
| NotificationGroupWithStatusJSON
|
||||
| ModerationWarningNotificationGroupJSON
|
||||
| AnnualReportNotificationGroupJSON;
|
||||
| AnnualReportNotificationGroupJSON
|
||||
| AddedToCollectionNotificationGroupJSON
|
||||
| CollectionUpdateNotificationGroupJSON;
|
||||
|
||||
export interface ApiNotificationGroupsResultJSON {
|
||||
accounts: ApiAccountJSON[];
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<AutosuggestEmoji /> > renders emoji with custom url 1`] = `
|
||||
<div
|
||||
className="autosuggest-emoji"
|
||||
>
|
||||
<img
|
||||
alt="foobar"
|
||||
className="emojione"
|
||||
src="http://example.com/emoji.png"
|
||||
/>
|
||||
<div
|
||||
className="autosuggest-emoji__name"
|
||||
>
|
||||
:foobar:
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<AutosuggestEmoji /> > renders native emoji 1`] = `
|
||||
<div
|
||||
className="autosuggest-emoji"
|
||||
>
|
||||
<img
|
||||
alt="💙"
|
||||
className="emojione"
|
||||
src="/emoji/1f499.svg"
|
||||
/>
|
||||
<div
|
||||
className="autosuggest-emoji__name"
|
||||
>
|
||||
:foobar:
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,29 +0,0 @@
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import AutosuggestEmoji from '../autosuggest_emoji';
|
||||
|
||||
describe('<AutosuggestEmoji />', () => {
|
||||
it('renders native emoji', () => {
|
||||
const emoji = {
|
||||
native: '💙',
|
||||
colons: ':foobar:',
|
||||
};
|
||||
const component = renderer.create(<AutosuggestEmoji emoji={emoji} />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders emoji with custom url', () => {
|
||||
const emoji = {
|
||||
custom: true,
|
||||
imageUrl: 'http://example.com/emoji.png',
|
||||
native: 'foobar',
|
||||
colons: ':foobar:',
|
||||
};
|
||||
const component = renderer.create(<AutosuggestEmoji emoji={emoji} />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@ import { openModal } from 'mastodon/actions/modal';
|
||||
import { initMuteModal } from 'mastodon/actions/mutes';
|
||||
import { apiFollowAccount } from 'mastodon/api/accounts';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { VerifiedBadge } from 'mastodon/components/badge';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { FollowersCounter } from 'mastodon/components/counters';
|
||||
import { DisplayName } from 'mastodon/components/display_name';
|
||||
@@ -29,7 +30,6 @@ import { FollowButton } from 'mastodon/components/follow_button';
|
||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
import { Skeleton } from 'mastodon/components/skeleton';
|
||||
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
||||
import { useIdentity } from 'mastodon/identity_context';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import type { MenuItem } from 'mastodon/models/dropdown_menu';
|
||||
@@ -277,7 +277,7 @@ export const Account: React.FC<AccountProps> = ({
|
||||
if (account?.mute_expires_at) {
|
||||
muteTimeRemaining = (
|
||||
<>
|
||||
· <RelativeTimestamp timestamp={account.mute_expires_at} />
|
||||
· <RelativeTimestamp hasFuture timestamp={account.mute_expires_at} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ import type { FC } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import IconPinned from '@/images/icons/icon_pinned.svg?react';
|
||||
import { fetchRelationships } from '@/mastodon/actions/accounts';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import type { AccountRole } from '@/mastodon/models/account';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
|
||||
import {
|
||||
AdminBadge,
|
||||
AutomatedBadge,
|
||||
@@ -14,13 +15,9 @@ import {
|
||||
BlockedBadge,
|
||||
GroupBadge,
|
||||
MutedBadge,
|
||||
} from '@/mastodon/components/badge';
|
||||
import { Icon } from '@/mastodon/components/icon';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import type { AccountRole } from '@/mastodon/models/account';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
} from '../badge';
|
||||
|
||||
import classes from './redesign.module.scss';
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
const account = useAccount(accountId);
|
||||
@@ -53,7 +50,6 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
<AdminBadge
|
||||
key={role.id}
|
||||
label={role.name}
|
||||
className={classes.badge}
|
||||
domain={`(${domain})`}
|
||||
roleId={role.id}
|
||||
/>,
|
||||
@@ -63,7 +59,6 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
<Badge
|
||||
key={role.id}
|
||||
label={role.name}
|
||||
className={classes.badge}
|
||||
domain={`(${domain})`}
|
||||
roleId={role.id}
|
||||
/>,
|
||||
@@ -72,25 +67,19 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
});
|
||||
|
||||
if (account.bot) {
|
||||
badges.push(<AutomatedBadge key='bot-badge' className={classes.badge} />);
|
||||
badges.push(<AutomatedBadge key='bot-badge' />);
|
||||
}
|
||||
if (account.group) {
|
||||
badges.push(<GroupBadge key='group-badge' className={classes.badge} />);
|
||||
badges.push(<GroupBadge key='group-badge' />);
|
||||
}
|
||||
if (relationship) {
|
||||
if (relationship.blocking) {
|
||||
badges.push(
|
||||
<BlockedBadge
|
||||
key='blocking'
|
||||
className={classNames(classes.badge, classes.badgeBlocked)}
|
||||
/>,
|
||||
);
|
||||
badges.push(<BlockedBadge key='blocking' />);
|
||||
}
|
||||
if (relationship.domain_blocking) {
|
||||
badges.push(
|
||||
<BlockedBadge
|
||||
key='domain-blocking'
|
||||
className={classNames(classes.badge, classes.badgeBlocked)}
|
||||
domain={domain}
|
||||
label={
|
||||
<FormattedMessage
|
||||
@@ -105,7 +94,6 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
badges.push(
|
||||
<MutedBadge
|
||||
key='muted-badge'
|
||||
className={classNames(classes.badge, classes.badgeMuted)}
|
||||
expiresAt={relationship.muting_expires_at}
|
||||
/>,
|
||||
);
|
||||
@@ -116,19 +104,9 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={'account__header__badges'}>{badges}</div>;
|
||||
return <div className={classes.badges}>{badges}</div>;
|
||||
};
|
||||
|
||||
export const PinnedBadge: FC = () => (
|
||||
<Badge
|
||||
className={classes.badge}
|
||||
icon={<Icon id='pinned' icon={IconPinned} />}
|
||||
label={
|
||||
<FormattedMessage id='account.timeline.pinned' defaultMessage='Pinned' />
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
function isAdminBadge(role: AccountRole) {
|
||||
const name = role.name.toLowerCase();
|
||||
return name === 'admin' || name === 'owner';
|
||||
140
app/javascript/mastodon/components/account_header/banners.tsx
Normal file
140
app/javascript/mastodon/components/account_header/banners.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { FC, ReactElement, ReactNode } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
authorizeFollowRequest,
|
||||
rejectFollowRequest,
|
||||
} from '@/mastodon/actions/accounts';
|
||||
import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility';
|
||||
import { useRelationship } from '@/mastodon/hooks/useRelationship';
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
|
||||
import { AvatarOverlay } from '../avatar_overlay';
|
||||
import { Button } from '../button';
|
||||
import { DisplayName } from '../display_name';
|
||||
import { Icon } from '../icon';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
export const AccountBanners: FC<{ account: Account }> = ({ account }) => {
|
||||
const { suspended, hidden } = useAccountVisibility(account.id);
|
||||
const relationship = useRelationship(account.id);
|
||||
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let banner: ReactNode = null;
|
||||
|
||||
if (account.memorial) {
|
||||
banner = (
|
||||
<MessageText>
|
||||
<FormattedMessage
|
||||
id='account.in_memoriam'
|
||||
defaultMessage='In Memoriam.'
|
||||
/>
|
||||
</MessageText>
|
||||
);
|
||||
}
|
||||
|
||||
if (account.moved) {
|
||||
banner = <MovedNote account={account} targetAccountId={account.moved} />;
|
||||
}
|
||||
|
||||
if (!suspended && relationship?.requested_by) {
|
||||
banner = <FollowRequestNote account={account} />;
|
||||
}
|
||||
|
||||
if (!banner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={classes.bannerWrapper}>{banner}</div>;
|
||||
};
|
||||
|
||||
const FollowRequestNote: FC<{ account: Account }> = ({ account }) => {
|
||||
const accountId = account.id;
|
||||
const dispatch = useAppDispatch();
|
||||
const handleAuthorize = useCallback(() => {
|
||||
dispatch(authorizeFollowRequest(accountId));
|
||||
}, [accountId, dispatch]);
|
||||
const handleReject = useCallback(() => {
|
||||
dispatch(rejectFollowRequest(accountId));
|
||||
}, [accountId, dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MessageText>
|
||||
<FormattedMessage
|
||||
id='account.requested_follow'
|
||||
defaultMessage='{name} has requested to follow you'
|
||||
values={{ name: <DisplayName account={account} variant='simple' /> }}
|
||||
/>
|
||||
</MessageText>
|
||||
|
||||
<div className={classes.bannerActions}>
|
||||
<Button secondary onClick={handleAuthorize}>
|
||||
<Icon id='check' icon={CheckIcon} />
|
||||
<FormattedMessage
|
||||
id='follow_request.authorize'
|
||||
defaultMessage='Authorize'
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Button secondary onClick={handleReject}>
|
||||
<Icon id='times' icon={CloseIcon} />
|
||||
<FormattedMessage
|
||||
id='follow_request.reject'
|
||||
defaultMessage='Reject'
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const MovedNote: React.FC<{
|
||||
account: Account;
|
||||
targetAccountId: string;
|
||||
}> = ({ account: from, targetAccountId }) => {
|
||||
const to = useAppSelector((state) => state.accounts.get(targetAccountId));
|
||||
|
||||
return (
|
||||
<>
|
||||
<MessageText>
|
||||
<FormattedMessage
|
||||
id='account.moved_to'
|
||||
defaultMessage='{name} has indicated that their new account is now:'
|
||||
values={{
|
||||
name: <DisplayName account={from} variant='simple' />,
|
||||
}}
|
||||
/>
|
||||
</MessageText>
|
||||
|
||||
<div className={classes.bannerActions}>
|
||||
<Link to={`/@${to?.acct}`} className={classes.bannerActionsDisplayName}>
|
||||
<AvatarOverlay account={to} friend={from} />
|
||||
<DisplayName account={to} />
|
||||
</Link>
|
||||
|
||||
<Link to={`/@${to?.acct}`} className='button'>
|
||||
<FormattedMessage
|
||||
id='account.go_to_profile'
|
||||
defaultMessage='Go to profile'
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const MessageText: React.FC<{ children: ReactElement }> = ({ children }) => (
|
||||
<div className={classes.bannerText}>{children}</div>
|
||||
);
|
||||
@@ -3,12 +3,7 @@ import type { FC } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { followAccount } from '@/mastodon/actions/accounts';
|
||||
import { CopyIconButton } from '@/mastodon/components/copy_icon_button';
|
||||
import { FollowButton } from '@/mastodon/components/follow_button';
|
||||
import { IconButton } from '@/mastodon/components/icon_button';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { getAccountHidden } from '@/mastodon/selectors/accounts';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
@@ -16,7 +11,12 @@ import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react
|
||||
import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react';
|
||||
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
|
||||
|
||||
import { CopyIconButton } from '../copy_icon_button';
|
||||
import { FollowButton } from '../follow_button';
|
||||
import { IconButton } from '../icon_button';
|
||||
|
||||
import { AccountMenu } from './menu';
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
enableNotifications: {
|
||||
@@ -48,7 +48,7 @@ export const AccountButtons: FC<AccountButtonsProps> = ({
|
||||
const me = useAppSelector((state) => state.meta.get('me') as string);
|
||||
|
||||
return (
|
||||
<div className={classNames('account__header__buttons', className)}>
|
||||
<div className={className}>
|
||||
{!hidden && (
|
||||
<AccountButtonsOther accountId={accountId} noShare={noShare} />
|
||||
)}
|
||||
@@ -93,7 +93,7 @@ const AccountButtonsOther: FC<
|
||||
{!isMovedAndUnfollowedAccount && (
|
||||
<FollowButton
|
||||
accountId={accountId}
|
||||
className='account__header__follow-button'
|
||||
className={classes.followButton}
|
||||
labelLength='long'
|
||||
/>
|
||||
)}
|
||||
@@ -7,23 +7,23 @@ import classNames from 'classnames';
|
||||
|
||||
import IconVerified from '@/images/icons/icon_verified.svg?react';
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||
import type { EmojiHTMLProps } from '@/mastodon/components/emoji/html';
|
||||
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
||||
import { Icon } from '@/mastodon/components/icon';
|
||||
import { IconButton } from '@/mastodon/components/icon_button';
|
||||
import { MiniCard } from '@/mastodon/components/mini_card';
|
||||
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
|
||||
import { useFieldHtml } from '@/mastodon/features/account_timeline/hooks/useFieldHtml';
|
||||
import { cleanExtraEmojis } from '@/mastodon/features/emoji/normalize';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useResizeObserver } from '@/mastodon/hooks/useObserver';
|
||||
import type { AccountFieldShape } from '@/mastodon/models/account';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
import MoreIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
|
||||
import { cleanExtraEmojis } from '../../emoji/normalize';
|
||||
import type { AccountField } from '../common';
|
||||
import { useFieldHtml } from '../hooks/useFieldHtml';
|
||||
import { CustomEmojiProvider } from '../emoji/context';
|
||||
import type { EmojiHTMLProps } from '../emoji/html';
|
||||
import { EmojiHTML } from '../emoji/html';
|
||||
import { Icon } from '../icon';
|
||||
import { IconButton } from '../icon_button';
|
||||
import { MiniCard } from '../mini_card';
|
||||
import { useElementHandledLink } from '../status/handled_link';
|
||||
|
||||
import classes from './redesign.module.scss';
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
const verifyMessage = defineMessage({
|
||||
id: 'account.link_verified_on',
|
||||
@@ -37,6 +37,12 @@ const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
||||
minute: '2-digit',
|
||||
};
|
||||
|
||||
export interface AccountField extends AccountFieldShape {
|
||||
nameHasEmojis: boolean;
|
||||
value_plain: string;
|
||||
valueHasEmojis: boolean;
|
||||
}
|
||||
|
||||
export const AccountHeaderFields: FC<{ accountId: string }> = ({
|
||||
accountId,
|
||||
}) => {
|
||||
@@ -102,11 +108,9 @@ const FieldCard: FC<{
|
||||
}> = ({ htmlHandlers, field }) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
name,
|
||||
name_emojified,
|
||||
nameHasEmojis,
|
||||
value_emojified,
|
||||
value_plain,
|
||||
valueHasEmojis,
|
||||
verified_at,
|
||||
} = field;
|
||||
@@ -132,8 +136,7 @@ const FieldCard: FC<{
|
||||
)}
|
||||
label={
|
||||
<FieldHTML
|
||||
text={name}
|
||||
textEmojified={name_emojified}
|
||||
text={name_emojified}
|
||||
textHasCustomEmoji={nameHasEmojis}
|
||||
className='translate'
|
||||
isOverflowing={isLabelOverflowing}
|
||||
@@ -143,8 +146,7 @@ const FieldCard: FC<{
|
||||
}
|
||||
value={
|
||||
<FieldHTML
|
||||
text={value_plain}
|
||||
textEmojified={value_emojified}
|
||||
text={value_emojified}
|
||||
textHasCustomEmoji={valueHasEmojis}
|
||||
isOverflowing={isValueOverflowing}
|
||||
onOverflowClick={handleOverflowClick}
|
||||
@@ -169,7 +171,6 @@ const FieldCard: FC<{
|
||||
|
||||
type FieldHTMLProps = {
|
||||
text: string;
|
||||
textEmojified: string;
|
||||
textHasCustomEmoji: boolean;
|
||||
isOverflowing?: boolean;
|
||||
onOverflowClick?: () => void;
|
||||
@@ -177,9 +178,7 @@ type FieldHTMLProps = {
|
||||
|
||||
const FieldHTML: FC<FieldHTMLProps> = ({
|
||||
className,
|
||||
extraEmojis,
|
||||
text,
|
||||
textEmojified,
|
||||
textHasCustomEmoji,
|
||||
isOverflowing,
|
||||
onOverflowClick,
|
||||
@@ -192,7 +191,7 @@ const FieldHTML: FC<FieldHTMLProps> = ({
|
||||
const html = (
|
||||
<EmojiHTML
|
||||
as='span'
|
||||
htmlString={textEmojified}
|
||||
htmlString={text}
|
||||
className={className}
|
||||
onElement={handleElement}
|
||||
data-contents
|
||||
@@ -285,11 +284,14 @@ function useColumnWrap() {
|
||||
if (!item) {
|
||||
break;
|
||||
}
|
||||
|
||||
const { ele, span } = item;
|
||||
|
||||
if (i < row.length - 1) {
|
||||
ele.dataset.cols = span.toString();
|
||||
remainingRowSpan -= span;
|
||||
} else if (
|
||||
row.length > 1 &&
|
||||
row.length === halfColSpan &&
|
||||
span === 1 &&
|
||||
remainingRowSpan > 1
|
||||
@@ -315,9 +317,10 @@ function useColumnWrap() {
|
||||
if (element) {
|
||||
listRef.current = element;
|
||||
observer.observe(element);
|
||||
handleRecalculate();
|
||||
}
|
||||
},
|
||||
[observer],
|
||||
[handleRecalculate, observer],
|
||||
);
|
||||
|
||||
return { wrapperRef: wrapperRefCallback };
|
||||
@@ -1,13 +1,10 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { Helmet } from '@unhead/react/helmet';
|
||||
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { AccountBio } from '@/mastodon/components/account_bio';
|
||||
import { Avatar } from '@/mastodon/components/avatar';
|
||||
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||
import FollowRequestNoteContainer from '@/mastodon/features/account/containers/follow_request_note_container';
|
||||
import { useLayout } from '@/mastodon/hooks/useLayout';
|
||||
import { useVisibility } from '@/mastodon/hooks/useVisibility';
|
||||
import {
|
||||
@@ -19,17 +16,19 @@ import type { Account } from '@/mastodon/models/account';
|
||||
import { getAccountHidden } from '@/mastodon/selectors/accounts';
|
||||
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
|
||||
|
||||
import { AccountName } from './account_name';
|
||||
import { AccountSubscriptionForm } from './account_subscription_form';
|
||||
import { AccountBadges } from './badges';
|
||||
import { AccountBio } from '../account_bio';
|
||||
import { Avatar } from '../avatar';
|
||||
import { AnimateEmojiProvider } from '../emoji/context';
|
||||
import { FamiliarFollowers } from '../familiar_followers';
|
||||
|
||||
import { AccountBanners } from './banners';
|
||||
import { AccountButtons } from './buttons';
|
||||
import { FamiliarFollowers } from './familiar_followers';
|
||||
import { AccountHeaderFields } from './fields';
|
||||
import { MemorialNote } from './memorial_note';
|
||||
import { MovedNote } from './moved_note';
|
||||
import { AccountNote as AccountNoteRedesign } from './note';
|
||||
import { AccountName } from './name';
|
||||
import { AccountNote } from './note';
|
||||
import { AccountNumberFields } from './number_fields';
|
||||
import redesignClasses from './redesign.module.scss';
|
||||
import classes from './styles.module.scss';
|
||||
import { AccountSubscriptionForm } from './subscription_form';
|
||||
import { AccountTabs } from './tabs';
|
||||
|
||||
const titleFromAccount = (account: Account) => {
|
||||
@@ -50,9 +49,6 @@ export const AccountHeader: React.FC<{
|
||||
}> = ({ accountId, hideTabs }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||
const relationship = useAppSelector((state) =>
|
||||
state.relationships.get(accountId),
|
||||
);
|
||||
const hidden = useAppSelector((state) => getAccountHidden(state, accountId));
|
||||
|
||||
const handleOpenAvatar = useCallback(
|
||||
@@ -96,27 +92,13 @@ export const AccountHeader: React.FC<{
|
||||
const isMe = me && account.id === me;
|
||||
|
||||
return (
|
||||
<div className='account-timeline__header'>
|
||||
{!hidden && account.memorial && <MemorialNote />}
|
||||
{!hidden && account.moved && (
|
||||
<MovedNote accountId={account.id} targetAccountId={account.moved} />
|
||||
)}
|
||||
<div>
|
||||
<AccountBanners account={account} />
|
||||
|
||||
<AnimateEmojiProvider
|
||||
className={classNames('account__header', {
|
||||
inactive: !!account.moved,
|
||||
})}
|
||||
className={classNames(!!account.moved && classes.moved)}
|
||||
>
|
||||
{!suspendedOrHidden && !account.moved && relationship?.requested_by && (
|
||||
<FollowRequestNoteContainer account={account} />
|
||||
)}
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
'account__header__image',
|
||||
redesignClasses.header,
|
||||
)}
|
||||
>
|
||||
<div className={classes.header}>
|
||||
{!suspendedOrHidden && (
|
||||
<img
|
||||
src={autoPlayGif ? account.header : account.header_static}
|
||||
@@ -126,26 +108,16 @@ export const AccountHeader: React.FC<{
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
'account__header__bar',
|
||||
redesignClasses.barWrapper,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'account__header__tabs',
|
||||
redesignClasses.avatarWrapper,
|
||||
)}
|
||||
>
|
||||
<div className={classes.barWrapper}>
|
||||
<div className={classes.avatarWrapper}>
|
||||
<a
|
||||
className='avatar'
|
||||
href={account.avatar}
|
||||
rel='noopener'
|
||||
target='_blank'
|
||||
onClick={handleOpenAvatar}
|
||||
>
|
||||
<Avatar
|
||||
className={classes.avatar}
|
||||
account={suspendedOrHidden ? undefined : account}
|
||||
alt={account.avatar_description}
|
||||
size={80}
|
||||
@@ -153,47 +125,36 @@ export const AccountHeader: React.FC<{
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
'account__header__tabs__name',
|
||||
redesignClasses.nameWrapper,
|
||||
)}
|
||||
>
|
||||
<div className={classes.displayNameWrapper}>
|
||||
<AccountName accountId={accountId} />
|
||||
<AccountButtons
|
||||
accountId={accountId}
|
||||
className={redesignClasses.buttonsDesktop}
|
||||
className={classes.buttonsDesktop}
|
||||
noShare={!isMe || 'share' in navigator}
|
||||
forceMenu={'share' in navigator}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AccountBadges accountId={accountId} />
|
||||
|
||||
<AccountNumberFields accountId={accountId} />
|
||||
|
||||
{!isMe && !suspendedOrHidden && (
|
||||
<FamiliarFollowers accountId={accountId} />
|
||||
<FamiliarFollowers
|
||||
accountId={accountId}
|
||||
className={classes.familiarFollowers}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!suspendedOrHidden && (
|
||||
<div className='account__header__extra'>
|
||||
<div className='account__header__bio'>
|
||||
{me && account.id !== me && (
|
||||
<AccountNoteRedesign accountId={accountId} />
|
||||
)}
|
||||
<div className={classes.bioButtonsWrapper}>
|
||||
{me && account.id !== me && <AccountNote accountId={accountId} />}
|
||||
|
||||
<AccountBio
|
||||
showDropdown
|
||||
accountId={accountId}
|
||||
className={classNames(
|
||||
'account__header__content',
|
||||
redesignClasses.bio,
|
||||
)}
|
||||
/>
|
||||
<AccountBio
|
||||
showDropdown
|
||||
accountId={accountId}
|
||||
className={classes.bio}
|
||||
/>
|
||||
|
||||
<AccountHeaderFields accountId={accountId} />
|
||||
</div>
|
||||
<AccountHeaderFields accountId={accountId} />
|
||||
|
||||
{!me && account.email_subscriptions && (
|
||||
<AccountSubscriptionForm accountId={accountId} />
|
||||
@@ -203,8 +164,8 @@ export const AccountHeader: React.FC<{
|
||||
|
||||
<AccountButtons
|
||||
className={classNames(
|
||||
redesignClasses.buttonsMobile,
|
||||
!isIntersecting && redesignClasses.buttonsMobileIsStuck,
|
||||
classes.buttonsMobile,
|
||||
!isIntersecting && classes.buttonsMobileIsStuck,
|
||||
)}
|
||||
accountId={accountId}
|
||||
noShare
|
||||
@@ -4,7 +4,6 @@ import type { FC } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import {
|
||||
blockAccount,
|
||||
followAccount,
|
||||
pinAccount,
|
||||
unblockAccount,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
} from '@/mastodon/actions/accounts';
|
||||
import { removeAccountFromFollowers } from '@/mastodon/actions/accounts_typed';
|
||||
import { showAlert } from '@/mastodon/actions/alerts';
|
||||
import { initBlockModal } from '@/mastodon/actions/blocks';
|
||||
import { directCompose, mentionCompose } from '@/mastodon/actions/compose';
|
||||
import {
|
||||
initDomainBlockModal,
|
||||
@@ -21,7 +21,10 @@ import {
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { initMuteModal } from '@/mastodon/actions/mutes';
|
||||
import { initReport } from '@/mastodon/actions/reports';
|
||||
import { Dropdown } from '@/mastodon/components/dropdown_menu';
|
||||
import {
|
||||
canAccountBeAdded,
|
||||
canAccountBeAddedByFollowers,
|
||||
} from '@/mastodon/features/collections/utils';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useIdentity } from '@/mastodon/identity_context';
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
@@ -40,7 +43,9 @@ import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react'
|
||||
import ReportIcon from '@/material-icons/400-24px/report.svg?react';
|
||||
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
|
||||
|
||||
import classes from './redesign.module.scss';
|
||||
import { Dropdown } from '../dropdown_menu';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
const intl = useIntl();
|
||||
@@ -61,7 +66,7 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
return [];
|
||||
}
|
||||
|
||||
return redesignMenuItems({
|
||||
return getMenuItems({
|
||||
account,
|
||||
signedIn: !isMe && signedIn,
|
||||
permissions,
|
||||
@@ -213,6 +218,10 @@ const redesignMessages = defineMessages({
|
||||
id: 'account.menu.add_to_list',
|
||||
defaultMessage: 'Add to list…',
|
||||
},
|
||||
addToCollection: {
|
||||
id: 'account.menu.add_to_collection',
|
||||
defaultMessage: 'Add to collection…',
|
||||
},
|
||||
openOriginalPage: {
|
||||
id: 'account.menu.open_original_page',
|
||||
defaultMessage: 'View on {domain}',
|
||||
@@ -223,7 +232,7 @@ const redesignMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
function redesignMenuItems({
|
||||
function getMenuItems({
|
||||
account,
|
||||
signedIn,
|
||||
permissions,
|
||||
@@ -293,35 +302,57 @@ function redesignMenuItems({
|
||||
return items;
|
||||
}
|
||||
|
||||
// List and featuring options
|
||||
// Add to list
|
||||
if (relationship?.following) {
|
||||
items.push(
|
||||
{
|
||||
text: intl.formatMessage(redesignMessages.addToList),
|
||||
action: () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'LIST_ADDER',
|
||||
modalProps: {
|
||||
accountId: account.id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
items.push({
|
||||
text: intl.formatMessage(redesignMessages.addToList),
|
||||
action: () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'LIST_ADDER',
|
||||
modalProps: {
|
||||
accountId: account.id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(
|
||||
relationship.endorsed ? messages.unendorse : messages.endorse,
|
||||
),
|
||||
action: () => {
|
||||
if (relationship.endorsed) {
|
||||
dispatch(unpinAccount(account.id));
|
||||
} else {
|
||||
dispatch(pinAccount(account.id));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Add to collection
|
||||
if (
|
||||
canAccountBeAdded(account) ||
|
||||
(canAccountBeAddedByFollowers(account) && relationship?.following)
|
||||
) {
|
||||
items.push({
|
||||
text: intl.formatMessage(redesignMessages.addToCollection),
|
||||
action: () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'COLLECTION_ADDER',
|
||||
modalProps: {
|
||||
accountId: account.id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Feature on profile
|
||||
if (relationship?.following) {
|
||||
items.push({
|
||||
text: intl.formatMessage(
|
||||
relationship.endorsed ? messages.unendorse : messages.endorse,
|
||||
),
|
||||
action: () => {
|
||||
if (relationship.endorsed) {
|
||||
dispatch(unpinAccount(account.id));
|
||||
} else {
|
||||
dispatch(pinAccount(account.id));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
items.push(
|
||||
@@ -435,7 +466,7 @@ function redesignMenuItems({
|
||||
if (relationship?.blocking) {
|
||||
dispatch(unblockAccount(account.id));
|
||||
} else {
|
||||
dispatch(blockAccount(account.id));
|
||||
dispatch(initBlockModal(account));
|
||||
}
|
||||
},
|
||||
dangerous: true,
|
||||
@@ -7,15 +7,22 @@ import classNames from 'classnames';
|
||||
|
||||
import Overlay from 'react-overlays/esm/Overlay';
|
||||
|
||||
import { DisplayName } from '@/mastodon/components/display_name';
|
||||
import { Icon } from '@/mastodon/components/icon';
|
||||
import { showAlert } from '@/mastodon/actions/alerts';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useAppSelector } from '@/mastodon/store';
|
||||
import { useRelationship } from '@/mastodon/hooks/useRelationship';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
import AtIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
|
||||
import HelpIcon from '@/material-icons/400-24px/help.svg?react';
|
||||
import DomainIcon from '@/material-icons/400-24px/language.svg?react';
|
||||
|
||||
import classes from './redesign.module.scss';
|
||||
import { FollowsYouBadge } from '../badge';
|
||||
import { Button } from '../button';
|
||||
import { DisplayName } from '../display_name';
|
||||
import { Icon } from '../icon';
|
||||
|
||||
import { AccountBadges } from './badges';
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
lockedInfo: {
|
||||
@@ -27,6 +34,10 @@ const messages = defineMessages({
|
||||
id: 'account.name_info',
|
||||
defaultMessage: 'What does this mean?',
|
||||
},
|
||||
copied: {
|
||||
id: 'copy_icon_button.copied',
|
||||
defaultMessage: 'Copied to clipboard',
|
||||
},
|
||||
});
|
||||
|
||||
export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
@@ -35,6 +46,7 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
const localDomain = useAppSelector(
|
||||
(state) => state.meta.get('domain') as string,
|
||||
);
|
||||
const relationship = useRelationship(accountId);
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
@@ -43,18 +55,21 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
const [username = '', domain = localDomain] = account.acct.split('@');
|
||||
|
||||
return (
|
||||
<div className={classes.name}>
|
||||
<h1>
|
||||
<DisplayName account={account} variant='simple' />
|
||||
</h1>
|
||||
<p className={classes.username}>
|
||||
@{username}@{domain}
|
||||
<AccountNameHelp
|
||||
username={username}
|
||||
domain={domain}
|
||||
isSelf={account.id === me}
|
||||
/>
|
||||
</p>
|
||||
<div className={classes.nameWrapper}>
|
||||
<div className={classes.name}>
|
||||
<h1>
|
||||
<DisplayName account={account} variant='simple' />
|
||||
</h1>
|
||||
{relationship?.followed_by && <FollowsYouBadge />}
|
||||
</div>
|
||||
|
||||
<AccountNameHelp
|
||||
username={username}
|
||||
domain={domain}
|
||||
isSelf={account.id === me}
|
||||
/>
|
||||
|
||||
<AccountBadges accountId={accountId} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -73,6 +88,19 @@ const AccountNameHelp: FC<{
|
||||
setOpen((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const handle = `@${username}@${domain}`;
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const handleCopy = useCallback(() => {
|
||||
void navigator.clipboard.writeText(handle);
|
||||
setCopied(true);
|
||||
dispatch(showAlert({ message: messages.copied }));
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 700);
|
||||
}, [handle, dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
@@ -83,6 +111,7 @@ const AccountNameHelp: FC<{
|
||||
aria-expanded={open}
|
||||
aria-controls={accessibilityId}
|
||||
>
|
||||
{handle}
|
||||
<Icon
|
||||
id='help'
|
||||
icon={HelpIcon}
|
||||
@@ -149,9 +178,25 @@ const AccountNameHelp: FC<{
|
||||
</ol>
|
||||
<FormattedMessage
|
||||
id='account.name.help.footer'
|
||||
defaultMessage='Just like you can send emails to people using different email clients, you can interact with people on other Mastodon servers – and with anyone on other social apps powered by the same set of rules as Mastodon uses (the ActivityPub protocol).'
|
||||
defaultMessage='Just like you can send emails to people using different email providers, you can interact with people on other Mastodon servers, and with anyone on other Fediverse apps.'
|
||||
tagName='p'
|
||||
/>
|
||||
|
||||
<Button onClick={handleCopy} className={classes.handleCopy}>
|
||||
<Icon id='copy' icon={ContentCopyIcon} />
|
||||
{!copied && (
|
||||
<FormattedMessage
|
||||
id='account.name.copy'
|
||||
defaultMessage='Copy handle'
|
||||
/>
|
||||
)}
|
||||
{copied && (
|
||||
<FormattedMessage
|
||||
id='copypaste.copied'
|
||||
defaultMessage='Copied'
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
@@ -5,12 +5,13 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { fetchRelationships } from '@/mastodon/actions/accounts';
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { Callout } from '@/mastodon/components/callout';
|
||||
import { IconButton } from '@/mastodon/components/icon_button';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
import EditIcon from '@/material-icons/400-24px/edit_square.svg?react';
|
||||
|
||||
import classes from './redesign.module.scss';
|
||||
import { Callout } from '../callout';
|
||||
import { IconButton } from '../icon_button';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
@@ -1,15 +1,17 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { FormattedDateWrapper } from '@/mastodon/components/formatted_date';
|
||||
import {
|
||||
NumberFields,
|
||||
NumberFieldsItem,
|
||||
} from '@/mastodon/components/number_fields';
|
||||
import { ShortNumber } from '@/mastodon/components/short_number';
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
|
||||
import { FormattedDateWrapper } from '../formatted_date';
|
||||
import { NumberFields, NumberFieldsItem } from '../number_fields';
|
||||
import { ShortNumber } from '../short_number';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
export const AccountNumberFields: FC<{ accountId: string }> = ({
|
||||
accountId,
|
||||
@@ -21,12 +23,19 @@ export const AccountNumberFields: FC<{ accountId: string }> = ({
|
||||
[account?.created_at],
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const showJoinModal = useCallback(() => {
|
||||
dispatch(
|
||||
openModal({ modalType: 'ACCOUNT_JOIN_DATE', modalProps: { accountId } }),
|
||||
);
|
||||
}, [accountId, dispatch]);
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NumberFields>
|
||||
<NumberFields className={classes.numberFields}>
|
||||
<NumberFieldsItem
|
||||
label={
|
||||
<FormattedMessage id='account.followers' defaultMessage='Followers' />
|
||||
@@ -60,15 +69,17 @@ export const AccountNumberFields: FC<{ accountId: string }> = ({
|
||||
}
|
||||
hint={intl.formatDate(account.created_at)}
|
||||
>
|
||||
{createdThisYear ? (
|
||||
<FormattedDateWrapper
|
||||
value={account.created_at}
|
||||
month='short'
|
||||
day='2-digit'
|
||||
/>
|
||||
) : (
|
||||
<FormattedDateWrapper value={account.created_at} year='numeric' />
|
||||
)}
|
||||
<button type='button' onClick={showJoinModal}>
|
||||
{createdThisYear ? (
|
||||
<FormattedDateWrapper
|
||||
value={account.created_at}
|
||||
month='short'
|
||||
day='2-digit'
|
||||
/>
|
||||
) : (
|
||||
<FormattedDateWrapper value={account.created_at} year='numeric' />
|
||||
)}
|
||||
</button>
|
||||
</NumberFieldsItem>
|
||||
</NumberFields>
|
||||
);
|
||||
@@ -1,62 +1,118 @@
|
||||
.moved {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
// Account header
|
||||
.header {
|
||||
height: 120px;
|
||||
overflow: hidden;
|
||||
background: var(--color-bg-secondary);
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
|
||||
img {
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@container (width >= 500px) {
|
||||
height: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.barWrapper {
|
||||
border-bottom: none;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
.avatarWrapper {
|
||||
margin-top: -64px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.nameWrapper {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
|
||||
> h1 {
|
||||
font-size: 22px;
|
||||
line-height: normal;
|
||||
white-space: initial;
|
||||
.moved & {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
// Wraps everything except the header image.
|
||||
.barWrapper {
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
// Avatar
|
||||
.avatarWrapper {
|
||||
margin-top: -64px;
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
align-items: center;
|
||||
user-select: all;
|
||||
margin-top: 4px;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
margin-inline-start: -2px; // aligns the pfp with content below
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--avatar-border-radius);
|
||||
|
||||
.moved & {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.displayNameWrapper {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
font-size: 17px;
|
||||
line-height: 22px;
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
:global(.emojione) {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.nameWrapper {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.name {
|
||||
> h1 {
|
||||
display: inline;
|
||||
font-size: 22px;
|
||||
line-height: normal;
|
||||
white-space: initial;
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.badges {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.handleHelpButton {
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: none;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
font-size: 1em;
|
||||
margin-left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 4px;
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
transition: color 0.2s ease-in-out;
|
||||
word-break: break-all;
|
||||
text-align: left;
|
||||
|
||||
> svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
@@ -74,6 +130,10 @@
|
||||
max-width: 400px;
|
||||
box-sizing: border-box;
|
||||
|
||||
[data-color-scheme='dark'] & {
|
||||
border: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
> h3 {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
@@ -91,15 +151,15 @@
|
||||
&:first-child {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
background: var(--color-bg-brand-softest);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 5px;
|
||||
border-radius: 9999px;
|
||||
box-sizing: border-box;
|
||||
> svg {
|
||||
background: var(--color-bg-brand-softest);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 5px;
|
||||
border-radius: 9999px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
@@ -107,9 +167,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
.handleCopy {
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
padding: 4px 8px;
|
||||
background-color: var(--color-bg-primary);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 13px;
|
||||
transition:
|
||||
color 0.2s ease-in-out,
|
||||
background-color 0.2s ease-in-out;
|
||||
margin-top: 12px;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: var(--color-bg-brand-softest);
|
||||
}
|
||||
}
|
||||
|
||||
$button-breakpoint: 420px;
|
||||
$button-fallback-breakpoint: $button-breakpoint + 55px;
|
||||
|
||||
.buttonsDesktop,
|
||||
.buttonsMobile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
:global(.button) {
|
||||
flex-shrink: 1;
|
||||
white-space: nowrap;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
:global(.icon-button) {
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: 4px;
|
||||
box-sizing: content-box;
|
||||
padding: 5px;
|
||||
|
||||
&:global(.copied) {
|
||||
border-color: var(--color-text-success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttonsDesktop {
|
||||
@container (width < #{$button-breakpoint}) {
|
||||
display: none;
|
||||
@@ -161,40 +265,36 @@ $button-fallback-breakpoint: $button-breakpoint + 55px;
|
||||
}
|
||||
}
|
||||
|
||||
.numberFields {
|
||||
@container (width >= #{$button-breakpoint}) {
|
||||
--number-fields-gap: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.bio {
|
||||
font-size: 15px;
|
||||
}
|
||||
color: var(--color-text-primary);
|
||||
unicode-bidi: plaintext;
|
||||
|
||||
.badge {
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: none;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
padding: 4px;
|
||||
font-size: 13px;
|
||||
p {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:global(.account__header__badges) > & {
|
||||
line-height: 1;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
font-weight: unset;
|
||||
opacity: 1;
|
||||
:any-link {
|
||||
color: var(--color-text-status-links);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badgeMuted {
|
||||
background-color: var(--color-bg-inverted);
|
||||
color: var(--color-text-inverted);
|
||||
}
|
||||
|
||||
.badgeBlocked {
|
||||
background-color: var(--color-bg-error-base);
|
||||
color: var(--color-text-on-error-base);
|
||||
}
|
||||
|
||||
svg.badgeIcon {
|
||||
opacity: 1;
|
||||
.familiarFollowers {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.note {
|
||||
@@ -273,8 +373,9 @@ svg.badgeIcon {
|
||||
.fieldVerified {
|
||||
background-color: var(--color-bg-success-softest);
|
||||
|
||||
dt {
|
||||
padding-right: 24px;
|
||||
&.fieldItem {
|
||||
border-color: var(--color-border-success-soft);
|
||||
padding-right: 32px; // 8px padding + 16px for the icon + 8px gap
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,63 +452,52 @@ svg.badgeIcon {
|
||||
}
|
||||
}
|
||||
|
||||
.tabs,
|
||||
.noTabs {
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 0 16px;
|
||||
|
||||
@container (width < 500px) {
|
||||
a {
|
||||
flex: 1 1 0px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
padding: 18px 4px;
|
||||
text-decoration: none;
|
||||
color: var(--color-text-primary);
|
||||
border-radius: 0;
|
||||
transition: color 0.2s ease-in-out;
|
||||
|
||||
&:not([aria-current='page']):is(:hover, :focus) {
|
||||
color: var(--color-text-brand-soft);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.active) {
|
||||
color: var(--color-text-brand);
|
||||
border-bottom: 4px solid var(--color-border-brand);
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.noTabs {
|
||||
width: 100%;
|
||||
border-width: 0 0 1px;
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
// Banners
|
||||
|
||||
.bannerWrapper,
|
||||
.bannerBase {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bannerWrapper {
|
||||
background: var(--color-bg-tertiary);
|
||||
padding: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bannerBase {
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background: var(--color-bg-secondary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.bannerBaseCentered {
|
||||
min-height: 146px;
|
||||
align-items: center;
|
||||
|
||||
.bannerTextAndActions {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.bannerText {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bannerTextAndActions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -422,22 +512,22 @@ svg.badgeIcon {
|
||||
}
|
||||
}
|
||||
|
||||
.bannerDisclaimer {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 11px;
|
||||
.bannerActions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.bannerBaseCentered {
|
||||
composes: bannerBase;
|
||||
min-height: 146px;
|
||||
align-items: center;
|
||||
|
||||
.bannerTextAndActions {
|
||||
text-align: center;
|
||||
.bannerDisclaimer {
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,8 +541,52 @@ svg.badgeIcon {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
:global(.button) {
|
||||
margin-top: 24px; // To align with input under label
|
||||
}
|
||||
|
||||
input[type='email'] {
|
||||
padding: 7px 8px; // To align size with button
|
||||
background: var(--color-bg-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.bannerActionsDisplayName {
|
||||
color: var(--color-text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover strong {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
strong,
|
||||
span {
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons
|
||||
|
||||
.followButton {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.bioButtonsWrapper {
|
||||
margin-top: 16px;
|
||||
}
|
||||
@@ -3,25 +3,24 @@ import { useState, useCallback, useId } from 'react';
|
||||
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||
import type { IntlShape } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
import { apiSubscribeByEmail } from 'mastodon/api/accounts';
|
||||
import { apiSubscribeByEmail } from '@/mastodon/api/accounts';
|
||||
import type {
|
||||
ValidationErrorResponse,
|
||||
ValidationError,
|
||||
} from 'mastodon/api_types/errors';
|
||||
import { A11yLiveRegion } from 'mastodon/components/a11y_live_region';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { CalloutInline } from 'mastodon/components/callout_inline';
|
||||
import { DisplayName } from 'mastodon/components/display_name';
|
||||
import type { FieldStatus } from 'mastodon/components/form_fields';
|
||||
import formFieldClasses from 'mastodon/components/form_fields/form_field_wrapper.module.scss';
|
||||
import { TextInput } from 'mastodon/components/form_fields/text_input_field';
|
||||
import { useAppSelector } from 'mastodon/store';
|
||||
} from '@/mastodon/api_types/errors';
|
||||
import { useAppSelector } from '@/mastodon/store';
|
||||
|
||||
import classes from './redesign.module.scss';
|
||||
import { Button } from '../button';
|
||||
import { DisplayName } from '../display_name';
|
||||
import type { FieldStatus } from '../form_fields';
|
||||
import { TextInputField } from '../form_fields/text_input_field';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
emailInvalid: {
|
||||
@@ -34,7 +33,7 @@ const messages = defineMessages({
|
||||
},
|
||||
email: {
|
||||
id: 'email_subscriptions.email',
|
||||
defaultMessage: 'Email address',
|
||||
defaultMessage: 'Email',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -105,8 +104,6 @@ export const AccountSubscriptionForm: React.FC<{ accountId: string }> = ({
|
||||
.then(() => {
|
||||
setSubmitting(false);
|
||||
setSubmitted(true);
|
||||
|
||||
return '';
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
setSubmitting(false);
|
||||
@@ -130,14 +127,15 @@ export const AccountSubscriptionForm: React.FC<{ accountId: string }> = ({
|
||||
|
||||
if (submitted) {
|
||||
return (
|
||||
<div className={classes.bannerBaseCentered}>
|
||||
<div
|
||||
className={classNames(classes.bannerBase, classes.bannerBaseCentered)}
|
||||
>
|
||||
<div className={classes.bannerTextAndActions}>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id='email_subscriptions.submitted.title'
|
||||
defaultMessage='One more step'
|
||||
/>
|
||||
</h2>
|
||||
<FormattedMessage
|
||||
id='email_subscriptions.submitted.title'
|
||||
defaultMessage='One more step'
|
||||
tagName='h2'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='email_subscriptions.submitted.lead'
|
||||
defaultMessage='Check your inbox for an email to finish signing up for email updates.'
|
||||
@@ -150,42 +148,27 @@ export const AccountSubscriptionForm: React.FC<{ accountId: string }> = ({
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={classes.bannerBase} noValidate>
|
||||
<div className={classes.bannerTextAndActions}>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id='email_subscriptions.form.title'
|
||||
defaultMessage='Sign up for email updates from {name}'
|
||||
values={{
|
||||
name: <DisplayName account={account} variant='simple' />,
|
||||
}}
|
||||
/>
|
||||
</h2>
|
||||
<FormattedMessage
|
||||
id='email_subscriptions.form.lead'
|
||||
defaultMessage='Get posts in your inbox without creating a Mastodon account.'
|
||||
id='email_subscriptions.form.title'
|
||||
defaultMessage='Sign up for email updates from {name}'
|
||||
tagName='h2'
|
||||
values={{
|
||||
name: <DisplayName account={account} variant='simple' />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={classes.bannerInputButton}>
|
||||
<div className={formFieldClasses.wrapper}>
|
||||
<TextInput
|
||||
id={`${accessibilityId}-input`}
|
||||
type='email'
|
||||
value={email}
|
||||
onChange={handleChange}
|
||||
placeholder='name@email.com'
|
||||
aria-label={intl.formatMessage(messages.email)}
|
||||
aria-describedby={errors.email ? `${accessibilityId}-status` : ''}
|
||||
/>
|
||||
|
||||
<A11yLiveRegion
|
||||
className={formFieldClasses.status}
|
||||
id={`${accessibilityId}-status`}
|
||||
>
|
||||
{errors.email && (
|
||||
<CalloutInline {...fieldStatusFromErrors(intl, errors.email)} />
|
||||
)}
|
||||
</A11yLiveRegion>
|
||||
</div>
|
||||
<TextInputField
|
||||
id={`${accessibilityId}-input`}
|
||||
type='email'
|
||||
value={email}
|
||||
onChange={handleChange}
|
||||
label={intl.formatMessage(messages.email)}
|
||||
status={
|
||||
errors.email ? fieldStatusFromErrors(intl, errors.email) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
<Button type='submit' loading={submitting}>
|
||||
<FormattedMessage
|
||||
@@ -197,8 +180,8 @@ export const AccountSubscriptionForm: React.FC<{ accountId: string }> = ({
|
||||
|
||||
<div className={classes.bannerDisclaimer}>
|
||||
<FormattedMessage
|
||||
id='email_subscriptions.form.disclaimer'
|
||||
defaultMessage='You can unsubscribe at any time. For more information, refer to the <a>Privacy Policy</a>.'
|
||||
id='email_subscriptions.form.bottom'
|
||||
defaultMessage='Get posts in your inbox without creating a Mastodon account. Unsubscribe at any time. For more information, refer to the <a>Privacy Policy</a>.'
|
||||
values={{ a: (str) => <Link to='/privacy-policy'>{str}</Link> }}
|
||||
/>
|
||||
</div>
|
||||
@@ -3,14 +3,13 @@ import type { FC } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import type { NavLinkProps } from 'react-router-dom';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
|
||||
import { areCollectionsEnabled } from '../../collections/utils';
|
||||
import { TabLink, TabList } from '../tab_list';
|
||||
|
||||
import classes from './redesign.module.scss';
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
const isActive: Required<NavLinkProps>['isActive'] = (match, location) =>
|
||||
match?.url === location.pathname ||
|
||||
@@ -30,27 +29,20 @@ export const AccountTabs: FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.tabs}>
|
||||
<NavLink isActive={isActive} to={`/@${acct}`}>
|
||||
<TabList>
|
||||
<TabLink isActive={isActive} to={`/@${acct}`}>
|
||||
<FormattedMessage id='account.activity' defaultMessage='Activity' />
|
||||
</NavLink>
|
||||
</TabLink>
|
||||
{show_media && (
|
||||
<NavLink exact to={`/@${acct}/media`}>
|
||||
<TabLink exact to={`/@${acct}/media`}>
|
||||
<FormattedMessage id='account.media' defaultMessage='Media' />
|
||||
</NavLink>
|
||||
</TabLink>
|
||||
)}
|
||||
{show_featured && (
|
||||
<NavLink exact to={`/@${acct}/featured`}>
|
||||
{areCollectionsEnabled() ? (
|
||||
<FormattedMessage
|
||||
id='account.featured.collections'
|
||||
defaultMessage='Collections'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage id='account.featured' defaultMessage='Featured' />
|
||||
)}
|
||||
</NavLink>
|
||||
<TabLink exact to={`/@${acct}/featured`}>
|
||||
<FormattedMessage id='account.featured' defaultMessage='Featured' />
|
||||
</TabLink>
|
||||
)}
|
||||
</div>
|
||||
</TabList>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { accountFactoryState, relationshipsFactory } from '@/testing/factories';
|
||||
|
||||
import { PendingBadge } from '../badge';
|
||||
|
||||
import { AccountListItem } from './index';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/AccountListItem',
|
||||
component: AccountListItem,
|
||||
args: {
|
||||
accountId: '1',
|
||||
withBorder: false,
|
||||
},
|
||||
parameters: {
|
||||
state: {
|
||||
accounts: {
|
||||
'1': accountFactoryState(),
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof AccountListItem>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const FollowsYou: Story = {
|
||||
parameters: {
|
||||
state: {
|
||||
relationships: {
|
||||
'1': relationshipsFactory({
|
||||
followed_by: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCustomStats: Story = {
|
||||
args: {
|
||||
stats: ['posts', 'last-active'],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCustomBadge: Story = {
|
||||
args: {
|
||||
badge: <PendingBadge />,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithBorder: Story = {
|
||||
args: {
|
||||
withBorder: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutButton: Story = {
|
||||
args: {
|
||||
renderButton: () => null,
|
||||
},
|
||||
};
|
||||
204
app/javascript/mastodon/components/account_list_item/index.tsx
Normal file
204
app/javascript/mastodon/components/account_list_item/index.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { FollowsYouBadge, VerifiedBadge } from 'mastodon/components/badge';
|
||||
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||
import { useRelationship } from 'mastodon/hooks/useRelationship';
|
||||
import { domain } from 'mastodon/initial_state';
|
||||
import type { Relationship } from 'mastodon/models/relationship';
|
||||
|
||||
import { Avatar } from '../avatar';
|
||||
import { useAccountHandle } from '../display_name/default';
|
||||
import { DisplayNameSimple } from '../display_name/simple';
|
||||
import { EmojiHTML } from '../emoji/html';
|
||||
import { FollowButton } from '../follow_button';
|
||||
import { FormattedDateWrapper } from '../formatted_date';
|
||||
import { ListItemLink, ListItemWrapper } from '../list_item';
|
||||
import { NumberFields, NumberFieldsItem } from '../number_fields';
|
||||
import { RelativeTimestamp } from '../relative_timestamp';
|
||||
import { ShortNumber } from '../short_number';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
export interface RenderButtonOptions {
|
||||
accountId: string | undefined;
|
||||
relationship: Relationship | null | undefined;
|
||||
}
|
||||
|
||||
type Stat = 'followers' | 'following' | 'posts' | 'joined' | 'last-active';
|
||||
|
||||
interface Props {
|
||||
accountId: string | undefined;
|
||||
stats?: Stat[];
|
||||
withBio?: boolean;
|
||||
withBorder?: boolean;
|
||||
badge?: ReactNode;
|
||||
renderButton?: (options: RenderButtonOptions) => React.ReactNode;
|
||||
}
|
||||
|
||||
const DEFAULT_STATS: Stat[] = ['followers', 'posts', 'last-active'];
|
||||
|
||||
/**
|
||||
* Extended account list item with bio, verified link badge,
|
||||
* and familiar follower widget.
|
||||
*
|
||||
* The displayed account stats can be customised using the `stats` prop,
|
||||
* and button rendering can be customised via the `renderButton` prop.
|
||||
*/
|
||||
export const AccountListItem: React.FC<Props> = ({
|
||||
accountId,
|
||||
stats = DEFAULT_STATS,
|
||||
withBio = true,
|
||||
withBorder = true,
|
||||
badge: badgeProp,
|
||||
renderButton = defaultRenderButton,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const account = useAccount(accountId);
|
||||
const handle = useAccountHandle(account, domain);
|
||||
const relationship = useRelationship(accountId);
|
||||
|
||||
const createdThisYear = useMemo(
|
||||
() => account?.created_at.includes(new Date().getFullYear().toString()),
|
||||
[account?.created_at],
|
||||
);
|
||||
|
||||
if (!accountId || !account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const badge =
|
||||
badgeProp ?? (relationship?.followed_by ? <FollowsYouBadge /> : null);
|
||||
|
||||
const firstVerifiedField = account.fields.find((item) => !!item.verified_at);
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper} data-with-border={withBorder}>
|
||||
<ListItemWrapper
|
||||
className={classes.main}
|
||||
icon={<Avatar account={account} size={40} />}
|
||||
sideContent={
|
||||
<span className={classes.button}>
|
||||
{renderButton({ accountId, relationship })}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<ListItemLink
|
||||
to={`/@${account.acct}`}
|
||||
data-hover-card-account={accountId}
|
||||
subtitle={<span className={classes.handle}>{handle}</span>}
|
||||
>
|
||||
<DisplayNameSimple
|
||||
account={account}
|
||||
className={classes.displayName}
|
||||
/>
|
||||
{badge && <span className={classes.badge}>{badge}</span>}
|
||||
</ListItemLink>
|
||||
</ListItemWrapper>
|
||||
|
||||
<NumberFields>
|
||||
{stats.includes('followers') && (
|
||||
<NumberFieldsItem
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account.followers'
|
||||
defaultMessage='Followers'
|
||||
/>
|
||||
}
|
||||
hint={intl.formatNumber(account.followers_count)}
|
||||
>
|
||||
<ShortNumber value={account.followers_count} />
|
||||
</NumberFieldsItem>
|
||||
)}
|
||||
{stats.includes('following') && (
|
||||
<NumberFieldsItem
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account.following'
|
||||
defaultMessage='Following'
|
||||
/>
|
||||
}
|
||||
hint={intl.formatNumber(account.following_count)}
|
||||
link={`/@${account.acct}/following`}
|
||||
>
|
||||
<ShortNumber value={account.following_count} />
|
||||
</NumberFieldsItem>
|
||||
)}
|
||||
{stats.includes('posts') && (
|
||||
<NumberFieldsItem
|
||||
label={
|
||||
<FormattedMessage id='account.posts' defaultMessage='Posts' />
|
||||
}
|
||||
hint={intl.formatNumber(account.statuses_count)}
|
||||
>
|
||||
<ShortNumber value={account.statuses_count} />
|
||||
</NumberFieldsItem>
|
||||
)}
|
||||
{stats.includes('joined') && (
|
||||
<NumberFieldsItem
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account.joined_short'
|
||||
defaultMessage='Joined'
|
||||
/>
|
||||
}
|
||||
hint={intl.formatDate(account.created_at)}
|
||||
>
|
||||
{createdThisYear ? (
|
||||
<FormattedDateWrapper
|
||||
value={account.created_at}
|
||||
month='short'
|
||||
day='2-digit'
|
||||
/>
|
||||
) : (
|
||||
<FormattedDateWrapper value={account.created_at} year='numeric' />
|
||||
)}
|
||||
</NumberFieldsItem>
|
||||
)}
|
||||
{stats.includes('last-active') && (
|
||||
<NumberFieldsItem
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account.last_active'
|
||||
defaultMessage='Last active'
|
||||
/>
|
||||
}
|
||||
>
|
||||
{account.last_status_at ? (
|
||||
<RelativeTimestamp long timestamp={account.last_status_at} />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</NumberFieldsItem>
|
||||
)}
|
||||
{firstVerifiedField && (
|
||||
<VerifiedBadge
|
||||
link={firstVerifiedField.value}
|
||||
className={classes.verifiedBadge}
|
||||
/>
|
||||
)}
|
||||
</NumberFields>
|
||||
{withBio && account.note.length > 0 && (
|
||||
<EmojiHTML
|
||||
className={classNames(classes.bio, 'translate')}
|
||||
htmlString={account.note_emojified}
|
||||
extraEmojis={account.emojis}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultRenderButton = ({ accountId }: RenderButtonOptions) => (
|
||||
<AccountListItemFollowButton accountId={accountId} />
|
||||
);
|
||||
|
||||
export const AccountListItemFollowButton: React.FC<{
|
||||
accountId: string | undefined;
|
||||
}> = ({ accountId }) => (
|
||||
<FollowButton compact labelLength='short' accountId={accountId} />
|
||||
);
|
||||
@@ -0,0 +1,55 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
|
||||
&[data-with-border='true'] {
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
--list-item-padding: 0;
|
||||
}
|
||||
|
||||
.displayName {
|
||||
// Spacing for badge
|
||||
margin-inline-end: 6px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
// Sort out vertical alignment next to name
|
||||
vertical-align: -4px;
|
||||
}
|
||||
|
||||
.handle {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.button {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.verifiedBadge {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
.bio {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
|
||||
:any-link {
|
||||
color: var(--color-text-status-links);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { useState, useCallback, useRef, useId } from 'react';
|
||||
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type {
|
||||
OffsetValue,
|
||||
UsePopperOptions,
|
||||
@@ -18,9 +20,10 @@ import classes from './styles.module.scss';
|
||||
const offset = [0, 4] as OffsetValue;
|
||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
||||
|
||||
export const AltTextBadge: React.FC<{ description: string }> = ({
|
||||
description,
|
||||
}) => {
|
||||
export const AltTextBadge: React.FC<{
|
||||
description: string;
|
||||
className?: string;
|
||||
}> = ({ description, className }) => {
|
||||
const intl = useIntl();
|
||||
const uniqueId = useId();
|
||||
const popoverId = `${uniqueId}-popover`;
|
||||
@@ -48,7 +51,7 @@ export const AltTextBadge: React.FC<{ description: string }> = ({
|
||||
<button
|
||||
type='button'
|
||||
ref={buttonRef}
|
||||
className='media-gallery__alt__label'
|
||||
className={classNames('media-gallery__alt__label', className)}
|
||||
onClick={handleClick}
|
||||
aria-expanded={open}
|
||||
aria-controls={popoverId}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { assetHost } from 'mastodon/utils/config';
|
||||
|
||||
import { unicodeMapping } from '../features/emoji/emoji_unicode_mapping_light';
|
||||
|
||||
export default class AutosuggestEmoji extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
emoji: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { emoji } = this.props;
|
||||
let url;
|
||||
|
||||
if (emoji.custom) {
|
||||
url = emoji.imageUrl;
|
||||
} else {
|
||||
const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
|
||||
|
||||
if (!mapping) {
|
||||
return null;
|
||||
}
|
||||
|
||||
url = `${assetHost}/emoji/${mapping.filename}.svg`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='autosuggest-emoji'>
|
||||
<img
|
||||
className='emojione'
|
||||
src={url}
|
||||
alt={emoji.native || emoji.colons}
|
||||
/>
|
||||
|
||||
<div className='autosuggest-emoji__name'>{emoji.colons}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
20
app/javascript/mastodon/components/autosuggest_emoji.tsx
Normal file
20
app/javascript/mastodon/components/autosuggest_emoji.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { Emoji } from './emoji';
|
||||
|
||||
interface LegacyEmoji {
|
||||
id: string;
|
||||
custom?: boolean;
|
||||
native?: string;
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
export const AutosuggestEmoji: FC<{ emoji: LegacyEmoji }> = ({ emoji }) => {
|
||||
const colons = `:${emoji.id}:`;
|
||||
return (
|
||||
<div className='autosuggest-emoji'>
|
||||
<Emoji code={emoji.native ?? colons} />
|
||||
<div className='autosuggest-emoji__name'>{colons}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -9,8 +9,9 @@ import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||
|
||||
import AutosuggestEmoji from './autosuggest_emoji';
|
||||
import { AutosuggestEmoji } from './autosuggest_emoji';
|
||||
import { AutosuggestHashtag } from './autosuggest_hashtag';
|
||||
import { LocalCustomEmojiProvider } from './emoji/context';
|
||||
|
||||
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
|
||||
let word;
|
||||
@@ -219,15 +220,17 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
||||
spellCheck={spellCheck}
|
||||
/>
|
||||
|
||||
<Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={this.input} popperConfig={{ strategy: 'fixed' }}>
|
||||
{({ props }) => (
|
||||
<div {...props}>
|
||||
<div className='autosuggest-textarea__suggestions' style={{ width: this.input?.clientWidth }}>
|
||||
{suggestions.map(this.renderSuggestion)}
|
||||
<LocalCustomEmojiProvider>
|
||||
<Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={this.input} popperConfig={{ strategy: 'fixed' }}>
|
||||
{({ props }) => (
|
||||
<div {...props}>
|
||||
<div className='autosuggest-textarea__suggestions' style={{ width: this.input?.clientWidth }}>
|
||||
{suggestions.map(this.renderSuggestion)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
)}
|
||||
</Overlay>
|
||||
</LocalCustomEmojiProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ import Textarea from 'react-textarea-autosize';
|
||||
|
||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||
|
||||
import AutosuggestEmoji from './autosuggest_emoji';
|
||||
import { AutosuggestEmoji } from './autosuggest_emoji';
|
||||
import { AutosuggestHashtag } from './autosuggest_hashtag';
|
||||
import { LocalCustomEmojiProvider } from './emoji/context';
|
||||
|
||||
const textAtCursorMatchesToken = (str, caretPosition) => {
|
||||
let word;
|
||||
@@ -218,15 +219,17 @@ const AutosuggestTextarea = forwardRef(({
|
||||
lang={lang}
|
||||
/>
|
||||
|
||||
<Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={textareaRef} popperConfig={{ strategy: 'fixed' }}>
|
||||
{({ props }) => (
|
||||
<div {...props}>
|
||||
<div className='autosuggest-textarea__suggestions' style={{ width: textareaRef.current?.clientWidth }}>
|
||||
{suggestions.map(renderSuggestion)}
|
||||
<LocalCustomEmojiProvider>
|
||||
<Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={textareaRef} popperConfig={{ strategy: 'fixed' }}>
|
||||
{({ props }) => (
|
||||
<div {...props}>
|
||||
<div className='autosuggest-textarea__suggestions' style={{ width: textareaRef.current?.clientWidth }}>
|
||||
{suggestions.map(renderSuggestion)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
)}
|
||||
</Overlay>
|
||||
</LocalCustomEmojiProvider>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,14 +2,20 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import CelebrationIcon from '@/material-icons/400-24px/celebration-fill.svg?react';
|
||||
|
||||
import * as badges from './badge';
|
||||
import * as badges from '.';
|
||||
|
||||
const meta = {
|
||||
component: badges.Badge,
|
||||
title: 'Components/Badge',
|
||||
args: {
|
||||
domain: '',
|
||||
label: undefined,
|
||||
},
|
||||
argTypes: {
|
||||
domain: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof badges.Badge>;
|
||||
|
||||
export default meta;
|
||||
@@ -29,6 +35,12 @@ export const Domain: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Verified: Story = {
|
||||
render() {
|
||||
return <badges.VerifiedBadge link='example.com' />;
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomIcon: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
@@ -5,34 +5,64 @@ import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import AdminIcon from '@/images/icons/icon_admin.svg?react';
|
||||
import ClockIcon from '@/images/icons/icon_clock.svg?react';
|
||||
import FollowerIcon from '@/images/icons/icon_follower.svg?react';
|
||||
import IconVerified from '@/images/icons/icon_verified.svg?react';
|
||||
import type { OnAttributeHandler } from '@/mastodon/utils/html';
|
||||
import BlockIcon from '@/material-icons/400-24px/block.svg?react';
|
||||
import GroupsIcon from '@/material-icons/400-24px/group.svg?react';
|
||||
import PersonIcon from '@/material-icons/400-24px/person.svg?react';
|
||||
import SmartToyIcon from '@/material-icons/400-24px/smart_toy.svg?react';
|
||||
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
|
||||
|
||||
interface BadgeProps {
|
||||
import { EmojiHTML } from '../emoji/html';
|
||||
import { Icon } from '../icon';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
interface BadgeProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
label: ReactNode;
|
||||
icon?: ReactNode;
|
||||
className?: string;
|
||||
domain?: ReactNode;
|
||||
roleId?: string;
|
||||
variant?:
|
||||
| 'default'
|
||||
| 'subtle'
|
||||
| 'inverted'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger';
|
||||
}
|
||||
|
||||
type PresetBadgeProps = Omit<
|
||||
BadgeProps,
|
||||
'label' | 'icon' | 'domain' | 'roleId'
|
||||
>;
|
||||
|
||||
export const Badge: FC<BadgeProps> = ({
|
||||
icon = <PersonIcon />,
|
||||
variant = 'default',
|
||||
label,
|
||||
className,
|
||||
domain,
|
||||
roleId,
|
||||
...otherProps
|
||||
}) => (
|
||||
<div
|
||||
className={classNames('account-role', className)}
|
||||
{...otherProps}
|
||||
className={classNames(
|
||||
classes.badge,
|
||||
!icon && classes.badgeWithoutIcon,
|
||||
classes[variant],
|
||||
className,
|
||||
)}
|
||||
data-account-role-id={roleId}
|
||||
>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
{domain && <span className='account-role__domain'>{domain}</span>}
|
||||
<span className={classes.content}>
|
||||
{label}
|
||||
{domain && <span className={classes.domain}> {domain}</span>}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -60,13 +90,32 @@ export const GroupBadge: FC<Partial<BadgeProps>> = ({ label, ...props }) => (
|
||||
/>
|
||||
);
|
||||
|
||||
export const AutomatedBadge: FC<{ className?: string }> = ({ className }) => (
|
||||
export const AutomatedBadge: FC<PresetBadgeProps> = (props) => (
|
||||
<Badge
|
||||
icon={<SmartToyIcon />}
|
||||
label={
|
||||
<FormattedMessage id='account.badges.bot' defaultMessage='Automated' />
|
||||
}
|
||||
className={className}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const FollowsYouBadge: FC<PresetBadgeProps> = (props) => (
|
||||
<Badge
|
||||
icon={<FollowerIcon />}
|
||||
label={
|
||||
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const PendingBadge: FC<PresetBadgeProps> = (props) => (
|
||||
<Badge
|
||||
variant='warning'
|
||||
icon={<ClockIcon />}
|
||||
label={<FormattedMessage id='account.pending' defaultMessage='Pending' />}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -89,6 +138,7 @@ export const MutedBadge: FC<
|
||||
return (
|
||||
<Badge
|
||||
icon={<VolumeOffIcon />}
|
||||
variant='inverted'
|
||||
label={
|
||||
label ??
|
||||
(formattedDate ? (
|
||||
@@ -111,6 +161,7 @@ export const MutedBadge: FC<
|
||||
export const BlockedBadge: FC<Partial<BadgeProps>> = ({ label, ...props }) => (
|
||||
<Badge
|
||||
icon={<BlockIcon />}
|
||||
variant='danger'
|
||||
label={
|
||||
label ?? (
|
||||
<FormattedMessage
|
||||
@@ -122,3 +173,31 @@ export const BlockedBadge: FC<Partial<BadgeProps>> = ({ label, ...props }) => (
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const onAttribute: OnAttributeHandler = (name, value, tagName) => {
|
||||
if (name === 'rel' && tagName === 'a') {
|
||||
if (value === 'me') {
|
||||
return null;
|
||||
}
|
||||
return [
|
||||
name,
|
||||
value
|
||||
.split(' ')
|
||||
.filter((x) => x !== 'me')
|
||||
.join(' '),
|
||||
];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const VerifiedBadge: React.FC<{ link: string; className?: string }> = ({
|
||||
link,
|
||||
className,
|
||||
}) => (
|
||||
<Badge
|
||||
variant='success'
|
||||
icon={<Icon id='verified' icon={IconVerified} noFill />}
|
||||
label={<EmojiHTML as='span' htmlString={link} onAttribute={onAttribute} />}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
70
app/javascript/mastodon/components/badge/styles.module.scss
Normal file
70
app/javascript/mastodon/components/badge/styles.module.scss
Normal file
@@ -0,0 +1,70 @@
|
||||
.badge {
|
||||
color: var(--color-text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
padding: 4px;
|
||||
gap: 4px;
|
||||
border-radius: 8px;
|
||||
align-items: center;
|
||||
|
||||
> svg {
|
||||
flex-shrink: 0;
|
||||
width: auto;
|
||||
height: 17px;
|
||||
fill: currentColor;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:not(.badgeWithoutIcon) {
|
||||
padding-inline-end: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.domain {
|
||||
opacity: 0.75;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.default {
|
||||
background-color: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.subtle {
|
||||
background-color: var(--color-bg-brand-softest);
|
||||
}
|
||||
|
||||
.feature {
|
||||
background-color: var(--color-bg-brand-base);
|
||||
color: var(--color-text-on-brand-base);
|
||||
}
|
||||
|
||||
.inverted {
|
||||
background-color: var(--color-bg-inverted);
|
||||
color: var(--color-text-inverted);
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: var(--color-bg-success-softest);
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: var(--color-bg-warning-softest);
|
||||
}
|
||||
|
||||
.danger {
|
||||
background-color: var(--color-bg-error-base);
|
||||
color: var(--color-text-on-error-base);
|
||||
}
|
||||
@@ -74,6 +74,24 @@ export const Compact: Story = {
|
||||
play: buttonTest,
|
||||
};
|
||||
|
||||
export const CompactSecondary: Story = {
|
||||
args: {
|
||||
compact: true,
|
||||
secondary: true,
|
||||
children: 'Compact secondary button',
|
||||
},
|
||||
play: buttonTest,
|
||||
};
|
||||
|
||||
export const CompactPlain: Story = {
|
||||
args: {
|
||||
compact: true,
|
||||
plain: true,
|
||||
children: 'Compact plain button',
|
||||
},
|
||||
play: buttonTest,
|
||||
};
|
||||
|
||||
export const Dangerous: Story = {
|
||||
args: {
|
||||
dangerous: true,
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.content,
|
||||
.body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -32,6 +37,8 @@
|
||||
|
||||
.body {
|
||||
flex-grow: 1;
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
@@ -87,7 +94,7 @@
|
||||
}
|
||||
|
||||
.variantSubtle {
|
||||
border: 1px solid var(--color-bg-brand-softest);
|
||||
border: 1px solid var(--color-border-brand-soft);
|
||||
background-color: var(--color-bg-primary);
|
||||
|
||||
.icon {
|
||||
|
||||
@@ -73,7 +73,7 @@ const BackButton: React.FC<{
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
title?: string;
|
||||
title?: React.ReactNode;
|
||||
icon?: string;
|
||||
iconComponent?: IconProp;
|
||||
active?: boolean;
|
||||
@@ -276,9 +276,11 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
</>
|
||||
);
|
||||
|
||||
const HeadingElement = hasTitle ? 'h1' : 'div';
|
||||
|
||||
const component = (
|
||||
<div className={wrapperClassName}>
|
||||
<h1 className={buttonClassName}>
|
||||
<HeadingElement className={buttonClassName}>
|
||||
{hasTitle && (
|
||||
<>
|
||||
{backButton}
|
||||
@@ -311,7 +313,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
{extraButton}
|
||||
{collapseButton}
|
||||
</div>
|
||||
</h1>
|
||||
</HeadingElement>
|
||||
|
||||
<div
|
||||
className={collapsibleClassName}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user