Compare commits

..

178 commits

Author SHA1 Message Date
30e91de97f Merge branch 'develop' into fedi-absturztau-be 2023-04-22 07:55:09 +02:00
4d954b48bc resolve merge conflicts 2023-02-12 09:20:49 +01:00
4919f32ce4 Merge branch 'develop' into fedi-absturztau-be 2023-01-21 09:12:47 +01:00
a69ed0898e Merge branch 'develop' into fedi-absturztau-be 2022-12-18 15:22:23 +01:00
59a3e3fcbb Merge branch 'develop' into fedi-absturztau-be 2022-11-27 09:57:22 +01:00
b6280ae97d resolve merge conflicts 2022-11-20 08:37:26 +01:00
1b190db9ed Merge branch 'develop' into fedi-absturztau-be 2022-10-19 17:48:12 +02:00
31fa6223ec resolve merge conflicts 2022-09-17 11:17:40 +02:00
dccf5ed1c0 resolve merge conflicts 2022-08-28 10:34:00 +02:00
504a11c9da Merge branch 'develop' into fedi-absturztau-be 2022-08-07 11:46:50 +02:00
d3d13d50d4 resolve merge conflicts 2022-07-26 12:22:07 +02:00
6b10df2d89 merged akkoma-fe develop 2022-07-26 10:44:11 +02:00
d237c3cc6d fix loading crash on emoji reacts 2022-06-11 11:21:48 +02:00
Sol Fisher Romanoff
65b13849b9 Add emoji name as title tag 2022-06-11 10:59:28 +02:00
sadposter
0d67481109 fix width 2022-06-11 10:59:14 +02:00
d2bce51941 resolve merge conflicts 2022-06-11 10:58:50 +02:00
07d4433e25 force width 2022-06-11 10:57:26 +02:00
66604e987c allow custom reactions 2022-06-11 10:57:16 +02:00
ca5d0bea60 allow customs 2022-06-11 10:57:06 +02:00
89a37d84b2 resolve merge conflicts 2022-06-11 10:55:23 +02:00
38158bd561 Merge branch 'develop' into fedi-absturztau-be 2022-05-21 13:56:07 +02:00
221aa7e0b4 resolve merge conflicts 2022-04-26 20:47:46 +02:00
ef986724ef add dom2component mixin 2022-04-03 19:50:02 +02:00
5ce9136d89 resolve merge conflicts 2022-04-03 19:03:36 +02:00
643e087404 resolve merge conflicts 2022-03-16 20:21:33 +01:00
Lynne
1b712bc1a0 Remove ruffle
This patch removes Ruffle completely, significantly speeding up the
loading of the frontend, making it a little more responsive,
and reducing its compressed size.
2021-12-15 21:10:12 +01:00
12db1d8719 Merge branch 'develop' into fedi-absturztau-be 2021-12-15 20:50:39 +01:00
b4b906689f add threading 2021-10-02 19:46:22 +02:00
d34cef391b resolve merge confilcts 2021-10-02 19:31:29 +02:00
Tusooa Zhu
fe0e7bd63c
Fix timeline jump when scrolling
Ref: tree-threading
2021-09-16 10:05:07 -04:00
Tusooa Zhu
78f573af47
Make replying and mediaPlaying controlled
$refs is not a reliable way to deal with child components under
tree threading as it is not reactive, but the children may change at
any time. The only good way seems to be making these states aggregated on
the conversation component.

Ref: tree-threading
2021-09-16 00:34:14 -04:00
Tusooa Zhu
12ed9a1799
Fix virtual scrolling for tree threading
Ref: tree-threading
2021-09-16 00:34:14 -04:00
Tusooa Zhu
0b576b64e2
Clean up debug code for tree threading 2021-09-10 15:24:23 -04:00
Tusooa Zhu
708633d6b1
Fix controlled status display toggles 2021-09-10 15:10:10 -04:00
Tusooa Zhu
59a2f65a33
Add English translations for other replies count 2021-09-10 15:10:10 -04:00
Tusooa Zhu
c069a9f799
Add other replies count for reply list link 2021-09-10 15:10:10 -04:00
Tusooa Zhu
c634e15305
Do not display replies inside status as link if there are no other replies 2021-09-10 15:10:03 -04:00
Tusooa Zhu
a1404f7735
Add English translation for position of other replies button pref 2021-09-09 00:26:08 -04:00
Tusooa Zhu
70ec3ad09e
Make position of other replies button a pref 2021-09-09 00:26:08 -04:00
Tusooa Zhu
8a4c0bfa18
Add English translation for show all conversation button improvement 2021-09-09 00:26:08 -04:00
Tusooa Zhu
00cc721344
Improve "show full conversation" interaction
Now we only show that button when there are other statuses out of sight
(other toplevel statuses exist outside of the current thread tree).
2021-09-09 00:26:08 -04:00
Tusooa Zhu
a917bdc34b
Make other replies button stretch along the row 2021-09-09 00:26:08 -04:00
Tusooa Zhu
4648bc6f72
Optimise thread ancestor borders 2021-09-09 00:26:08 -04:00
Tusooa Zhu
5643314922
Optimise thread ancestor display style 2021-09-09 00:26:08 -04:00
Tusooa Zhu
89761743b8
Reset thread open state when collapsed 2021-09-09 00:26:07 -04:00
Tusooa Zhu
e01445b1d9
Clean up 2021-09-09 00:26:07 -04:00
Tusooa Zhu
5a37ea5a8e
Highlight ancestor of the current status when diving back to top 2021-09-09 00:26:07 -04:00
Tusooa Zhu
1fdba77ae5
Add English translation for Misskey-style tree view 2021-09-09 00:26:07 -04:00
Tusooa Zhu
7dd5e0fc8e
Implement Misskey-style tree view
Now the tree will be always rooted at the highlighted status, and
all its ancestors shown linearly on the top.

Enhancement: If an ancestor has more
than one reply (i.e. it has a child that is not on current status's
ancestor chain), we are given a link to root the thread at that status.
2021-09-09 00:26:07 -04:00
Tusooa Zhu
38a67cc2e2
Remove horizontal border and thicken vertical border in a thread tree 2021-09-09 00:26:07 -04:00
Tusooa Zhu
2c91a020bc
Make dive/undive button clickable along the whole row 2021-09-09 00:23:48 -04:00
Tusooa Zhu
2a6f0c4a34
Optimize thread border radius 2021-09-09 00:23:48 -04:00
Tusooa Zhu
f8d25d2c2a
Optimize thread display 2021-09-09 00:23:48 -04:00
Tusooa Zhu
b7e179c1fe
Fix the bug where toggleShowingTall does not work 2021-09-09 00:23:47 -04:00
Tusooa Zhu
46feecee70
Fix status undefined in parentOf 2021-09-09 00:23:09 -04:00
Tusooa Zhu
9029344e1f
Add English translations for max depth in thread 2021-09-09 00:22:53 -04:00
Tusooa Zhu
891fb00107
Add settings for max depth in thread 2021-09-09 00:22:53 -04:00
Tusooa Zhu
640d3041ef
Use mergedConfig properly 2021-09-09 00:22:32 -04:00
Tusooa Zhu
28469988dd
Fallback to simpleTree style 2021-09-09 00:22:15 -04:00
Tusooa Zhu
ac464be063
Undive when collapsed 2021-09-09 00:21:37 -04:00
Tusooa Zhu
c221805695
Clean up 2021-09-09 00:21:23 -04:00
Tusooa Zhu
49398566d3
Lint 2021-09-09 00:21:14 -04:00
Tusooa Zhu
9ccc374feb
Add English translation for simple tree 2021-09-09 00:21:00 -04:00
Tusooa Zhu
ad5c0abe14
Add simple tree style navigation 2021-09-09 00:20:51 -04:00
Tusooa Zhu
ff59c5a785
Add English translation for diving 2021-09-09 00:20:29 -04:00
Tusooa Zhu
93bcee9516
Add dive functionality 2021-09-09 00:20:22 -04:00
Tusooa Zhu
f0c717ce5f
Add English translations for diving 2021-09-09 00:19:15 -04:00
Tusooa Zhu
4f775b6be4
Support diving into one status in a conversation 2021-09-09 00:19:15 -04:00
Tusooa Zhu
436597a906
Make status display controlled 2021-09-09 00:19:15 -04:00
Tusooa Zhu
e6120df550
Add English translations for message threading 2021-09-09 00:19:15 -04:00
Tusooa Zhu
a4a0858110
Make show full thread message account for numbers 2021-09-09 00:19:15 -04:00
Tusooa Zhu
b1f4b1bff3
Implement thread folding/expanding 2021-09-09 00:19:15 -04:00
Tusooa Zhu
814a27749a
Add tree-style thread display 2021-09-09 00:19:15 -04:00
a0cf440fac swap of react and fav 2021-08-07 15:01:13 +02:00
d5e091e9af update readme 2021-08-07 11:55:40 +02:00
5a5320b71c Merge branch 'tusooa/pleroma-fe-from/develop/tusooa/media-touch-actions' into fedi-absturztau-be 2021-08-07 11:54:30 +02:00
7fe7339ea7 resolve merge conflicts 2021-08-07 11:46:39 +02:00
Tusooa Zhu
3b6b8b96c4
Bump pinch-zoom-element version 2021-08-02 23:25:38 -04:00
Tusooa Zhu
4c2a7aabe5
Clean up 2021-08-02 22:47:19 -04:00
Tusooa Zhu
3366c915e9
Check whether we swiped only for mouse pointer 2021-08-02 22:29:49 -04:00
Tusooa Zhu
db71bbf358
Scale swipe threshold with viewport width 2021-08-02 22:29:48 -04:00
Tusooa Zhu
7b77c0c884
Update pinch-zoom-element 2021-08-02 22:29:48 -04:00
Tusooa Zhu
1478e72fcc
Allow pinch-zoom to fill the whole screen 2021-08-02 22:29:48 -04:00
Tusooa Zhu
33384af5df
Use native click for hiding overlay
The pointerup strategy is unsuccessful, as some other overlays
(Firefox's Inspect Element) will pass down pointerup events.
2021-08-02 22:29:48 -04:00
Tusooa Zhu
9d4dd64276
Reset position on swipe end even if we cannot navigate 2021-08-02 22:29:48 -04:00
Tusooa Zhu
e2f3929872
Make lint happy 2021-08-02 22:29:47 -04:00
Tusooa Zhu
4552c28e06
Prevent the click event from firing on content below modal 2021-08-02 22:29:47 -04:00
Tusooa Zhu
214338b9f0
Add missing swipe click component 2021-08-02 22:29:47 -04:00
Tusooa Zhu
cd9384adae
Clean up 2021-08-02 22:29:47 -04:00
Tusooa Zhu
7469849c39
Add swipe-click handler to media modal
Now swiping will correctly change the current media, and with a good
preview. Clicking without swiping closes the overlay.
2021-08-02 22:29:47 -04:00
Tusooa Zhu
0a63208da7
Use pinch-zoom-element for pinch zoom functionality 2021-08-02 22:29:46 -04:00
Tusooa Zhu
0421c06d47
Add pan threshold 2021-08-02 22:29:46 -04:00
Tusooa Zhu
7d767f840b
Handle pinch action 2021-08-02 22:29:46 -04:00
Tusooa Zhu
aa70c31950
Preview swipe action 2021-08-02 22:29:46 -04:00
Tusooa Zhu
61509d1b1e
Make media modal be aware of multi-touch actions
Originally the media viewer would think every touch is a swipe (one-finger
touch event), so we would encounter the case where a two-finger scale event
would incorrectly change the current media. This is now fixed.
2021-08-02 22:28:16 -04:00
2b6974dcad fix chats 2021-07-04 19:04:12 +02:00
6594cdaa63 remove sorting because performance 2021-07-03 17:29:35 +02:00
b5cc8ec3ae update readme 2021-07-03 17:25:46 +02:00
0fc86d5bfe add tags as buttons to posts 2021-07-03 17:23:25 +02:00
77f8537f61 improved sidebar ordering when 3rd column is active 2021-06-28 22:54:33 +02:00
240bce92cd Merge branch 'fix-themes-select' into fedi-absturztau-be 2021-06-28 22:47:31 +02:00
9be1cc34ba temporary fix for external links 2021-06-28 21:51:12 +02:00
3336572a74 remove order style when sidebarAlign is active 2021-06-21 14:05:01 +02:00
fcf1c7b4b4 resolve merge conflicts 2021-06-21 13:58:30 +02:00
c8e517828e Merge branch 'develop' into fedi-absturztau-be 2021-05-12 13:50:27 +02:00
c44f2bbcd6 Merge branch 'develop' into fedi-absturztau-be 2021-04-19 11:45:22 +02:00
6064e31c5d Merge branch 'develop' into fedi-absturztau-be 2021-03-31 11:02:05 +02:00
97113bc5df minor fix to sub menu in nav panel 2021-03-16 11:29:03 +01:00
7f05730e9b resolve merge conflicts and update fixes 2021-03-16 11:20:22 +01:00
6ef591bdcc Merge branch 'develop' into fedi-absturztau-be 2021-03-09 17:14:31 +01:00
6e43a7d23c update readme 2021-02-28 01:03:48 +01:00
862a5a86e7 compact user panel 2021-02-28 00:40:54 +01:00
f304fed1e9 added compact nav panel 2021-02-28 00:09:30 +01:00
5ef3133f75 fix move notification column checkbox 2021-02-20 17:06:29 +01:00
0cfd263ef4 Merge branch 'develop' into fedi-absturztau-be 2021-02-13 10:59:52 +01:00
94c96345e1 fix language stuff 2021-01-30 10:58:06 +01:00
ba808076ca resolve merge conflicts 2021-01-30 10:22:55 +01:00
eaaa64dde8 ble 2021-01-09 20:06:06 +01:00
6b6558d924 add changes in readme 2021-01-09 20:04:54 +01:00
ecd5d78bc2 add zero width space when padding of emojis is disabled 2021-01-09 19:58:30 +01:00
a85c1d562c resolve merge conflicts 2021-01-08 17:36:47 +01:00
2a71d425cd change repo paths 2020-12-29 13:02:33 +01:00
381c4e7f82 resolve merge conflicts 2020-12-29 13:00:48 +01:00
026dc263e5 Merge branch 'develop' into fedi-absturztau-be 2020-12-16 12:00:58 +01:00
e15095d0dd Merge branch 'develop' into fedi-absturztau-be 2020-12-07 08:02:54 +01:00
ad728d9046 resolve conflichts 2020-12-04 20:45:10 +01:00
a6822d26bb Merge branch 'develop' into fedi-absturztau-be 2020-11-23 09:21:56 +01:00
8065afc4d7 resolve merge conflicts 2020-11-08 14:04:17 +01:00
a43623c5a5 Merge branch 'develop' into fedi-absturztau-be 2020-10-29 09:24:25 +01:00
7a9bd70a09 Merge branch 'develop' into fedi-absturztau-be 2020-10-07 18:51:14 +02:00
575ce2c65e Merge branch 'develop' into fedi-absturztau-be 2020-09-11 20:47:04 +02:00
07853ed322 Merge branch 'develop' into fedi-absturztau-be 2020-08-27 21:41:14 +02:00
aabd393e15 add settings for third column 2020-07-26 20:36:03 +02:00
1e65016d71 fix flex 2020-07-26 12:50:47 +02:00
2478d8b04e change width 2020-07-26 12:20:17 +02:00
2e2a261e3d move features panel to the right 2020-07-26 12:15:00 +02:00
ee67b579c5 add 3rd column for testing 2020-07-26 12:10:06 +02:00
6a77b27a42 resolve merge conflicts 2020-07-26 11:17:14 +02:00
e77cd1d625 fix pleroma-mod-loader ref 2020-07-08 22:10:04 +02:00
f2f11f7f63 add to language switcher 2020-07-08 21:57:34 +02:00
7668321c98 add en nyan 2020-07-08 21:55:58 +02:00
2cd6d336ba remove dublicate 2020-07-08 21:53:44 +02:00
ee45f689ac remove dublicate add mod loader 2020-07-08 21:48:08 +02:00
7b0425c985 Merge branch 'direct-conversations' into fedi-absturztau-be 2020-07-08 21:45:23 +02:00
7fa2dfecc6 Merge branch 'direct-conversations' of https://git.pleroma.social/eugenijm/pleroma-fe into direct-conversations 2020-07-08 19:16:44 +02:00
eugenijm
0d7d8e9f33 Submit on enter fixes 2020-05-30 20:26:24 +03:00
eugenijm
f2b3d1e6b0 Use streaming for real time updates instead of notifications, disable
polling when streaming is enabled.
2020-05-30 17:59:35 +03:00
eugenijm
6c2c0883ce Use since_id when pulling the current chat messages 2020-05-30 13:47:48 +03:00
eugenijm
fdef990c6a Submit on enter, start a new line on shift+enter 2020-05-29 09:43:52 +03:00
eugenijm
5fc9ef0bfb WIP chat fixes 2020-05-29 09:43:52 +03:00
eugenijm
b05a86b280 WIP keep the on screen keyboard after a message send. 2020-05-29 09:43:52 +03:00
eugenijm
5996954c4a Reset the chat list on logout 2020-05-29 09:43:52 +03:00
eugenijm
c82436cd8a WIP Chat refactoring 2020-05-29 09:43:52 +03:00
eugenijm
93049e9d52 WIP preserve scroll position during posting form resize, attachment upload, window resize, disable auto-focus for mobile 2020-05-29 09:43:52 +03:00
eugenijm
5f7494f134 Chat header alignment fixes for the mobile layout 2020-05-29 09:43:52 +03:00
eugenijm
c1faeacee8 Set file limit in the chat posting form 2020-05-29 09:43:52 +03:00
eugenijm
6b272b9c99 WIP chat improvements 2020-05-29 09:43:52 +03:00
eugenijm
3dbe0d1e61 WIP display updated_at in the chat list, use updated_at to keep the chat list sorted 2020-05-29 09:43:52 +03:00
eugenijm
cf4d3ee1bf WIP display attachment type in the last message 2020-05-29 09:43:52 +03:00
eugenijm
c9870a9d43 WIP keep last_message updated 2020-05-29 09:43:52 +03:00
eugenijm
09da927def WIP chat refactoring 2020-05-29 09:43:52 +03:00
eugenijm
99de78b80e WIP chat 2020-05-29 09:43:52 +03:00
eugenijm
d2456d5fea Chat message deletion 2020-05-29 09:43:52 +03:00
eugenijm
855f36eeda wip 2020-05-29 09:43:52 +03:00
eugenijm
1b51b02483 WIP chat fixes 2020-05-29 09:43:52 +03:00
eugenijm
6c487f0c58 WIP attachments 2020-05-29 09:43:52 +03:00
eugenijm
fc3d7a7565 WIP: add custom emoji 2020-05-29 09:43:52 +03:00
eugenijm
8a8a9ad562 WIP notification count 2020-05-29 09:43:52 +03:00
eugenijm
d48b7a7d58 WIP chat 2020-05-29 09:43:52 +03:00
eugenijm
5fdb5c554d WIP load chat messages on scroll up 2020-05-29 09:43:52 +03:00
eugenijm
86cc4ce08d WIP attachments 2020-05-29 09:43:52 +03:00
eugenijm
c273560408 WIP display the last message in the chat list 2020-05-29 09:43:52 +03:00
eugenijm
0f3c667eb8 wip 2020-05-29 09:43:52 +03:00
eugenijm
bd5b3e8922 wip 2020-05-29 09:43:52 +03:00
eugenijm
aa1f2a1c3f wip 2020-05-29 09:43:52 +03:00
eugenijm
5e93ca3edb WIP chat avatars 2020-05-29 09:43:52 +03:00
eugenijm
f06ec18cd7 Chat fixes 2020-05-29 09:43:52 +03:00
eugenijm
804cf3abc5 WIP: Chats 2020-05-29 09:43:52 +03:00
201 changed files with 7473 additions and 9547 deletions

2
.eslintignore Normal file
View file

@ -0,0 +1,2 @@
build/*.js
config/*.js

30
.eslintrc.js Normal file
View file

@ -0,0 +1,30 @@
module.exports = {
root: true,
parserOptions: {
parser: '@babel/eslint-parser',
sourceType: 'module'
},
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: [
'plugin:vue/recommended'
],
// required to lint *.vue files
plugins: [
'vue',
'import'
],
// add your custom rules here
rules: {
// allow paren-less arrow functions
'arrow-parens': 0,
// allow async-await
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'vue/require-prop-types': 0,
'vue/no-unused-vars': 0,
'no-tabs': 0,
'vue/multi-word-component-names': 0,
'vue/no-reserved-component-names': 0
}
}

1
.node-version Normal file
View file

@ -0,0 +1 @@
7.2.1

View file

@ -1 +0,0 @@
nodejs 20.12.2

View file

@ -1,25 +1,23 @@
labels: pipeline:
platform: linux/amd64
steps:
lint: lint:
when: when:
event: event:
- pull_request - pull_request
image: node:20 image: node:18
commands: commands:
- yarn - yarn
- yarn lint - yarn lint
#- yarn stylelint
test: test:
when: when:
event: event:
- pull_request - pull_request
image: node:20 image: node:18
commands: commands:
- apt update - apt update
- apt install firefox-esr -y --no-install-recommends - apt install firefox-esr -y --no-install-recommends
- yarn - yarn
- yarn unit - yarn unit
build: build:
@ -29,7 +27,7 @@ steps:
branch: branch:
- develop - develop
- stable - stable
image: node:20 image: node:18
commands: commands:
- yarn - yarn
- yarn build - yarn build
@ -41,15 +39,15 @@ steps:
branch: branch:
- develop - develop
- stable - stable
image: node:20 image: node:18
secrets: secrets:
- SCW_ACCESS_KEY - SCW_ACCESS_KEY
- SCW_SECRET_KEY - SCW_SECRET_KEY
- SCW_DEFAULT_ORGANIZATION_ID - SCW_DEFAULT_ORGANIZATION_ID
commands: commands:
- apt-get update && apt-get install -y rclone wget zip - apt-get update && apt-get install -y rclone wget zip
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64 - wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
- mv scaleway-cli_2.30.0_linux_amd64 scaleway-cli - mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
- chmod +x scaleway-cli - chmod +x scaleway-cli
- ./scaleway-cli object config install type=rclone - ./scaleway-cli object config install type=rclone
- zip akkoma-fe.zip -r dist - zip akkoma-fe.zip -r dist
@ -71,8 +69,8 @@ steps:
- SCW_DEFAULT_ORGANIZATION_ID - SCW_DEFAULT_ORGANIZATION_ID
commands: commands:
- apt-get update && apt-get install -y rclone wget git zip - apt-get update && apt-get install -y rclone wget git zip
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64 - wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
- mv scaleway-cli_2.30.0_linux_amd64 scaleway-cli - mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
- chmod +x scaleway-cli - chmod +x scaleway-cli
- ./scaleway-cli object config install type=rclone - ./scaleway-cli object config install type=rclone
- cd docs - cd docs
@ -80,4 +78,4 @@ steps:
- mkdocs build - mkdocs build
- zip -r docs.zip site/* - zip -r docs.zip site/*
- cd site - cd site
- rclone copy . scaleway:akkoma-docs/frontend/$CI_COMMIT_BRANCH/ - rclone copy . scaleway:akkoma-docs/frontend/$CI_COMMIT_BRANCH/

View file

@ -1,4 +1,4 @@
# Akkoma-FE # Akkoma-FE
![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet) ![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet)
@ -6,9 +6,22 @@ This is a fork of Akkoma-FE from the Pleroma project, with support for new Akkom
- MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm) - MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm)
- Custom emoji reactions - Custom emoji reactions
# Changes in this Fork
* script tag in index.html for [pleroma-mod-loader](https://git.pleroma.social/absturztaube/pleroma-mod-loader)
* ability to move notifications to a seperate column
* insert zero width space when padding of emojis is disabled
* add custom language "English (Nyan)"
* pointing version links to my gitlab repos
* optional compact styles provided by craftplacer
* tags as buttons bellow a post
* [pinch and pan media](https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1403)
* swap of react and favorite button in status
* adding __vueComponent__ to DOM nodes to make new mods work
# For Translators # For Translators
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Akkoma-FE. The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Akkoma-FE.
Alternatively, edit/create `src/i18n/$LANGUAGE_CODE.json` (where `$LANGUAGE_CODE` is the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language), then add your language to [src/i18n/messages.js](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/i18n/messages.js) if it doesn't already exist there. Alternatively, edit/create `src/i18n/$LANGUAGE_CODE.json` (where `$LANGUAGE_CODE` is the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language), then add your language to [src/i18n/messages.js](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/i18n/messages.js) if it doesn't already exist there.
@ -20,11 +33,9 @@ To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/ad
## Build Setup ## Build Setup
Make sure you have [Node.js](https://nodejs.org/) installed. You can check `/.woodpecker.yml` for which node version the Akkoma CI currently uses.
``` bash ``` bash
# install dependencies # install dependencies
corepack enable npm install -g yarn
yarn yarn
# serve with hot reload at localhost:8080 # serve with hot reload at localhost:8080
@ -39,7 +50,7 @@ npm run unit
# For Contributors: # For Contributors:
You can create file `/config/local.json` (see [example](https://akkoma.dev/AkkomaGang/akkoma-fe/src/branch/develop/config/local.example.json)) to enable some convenience dev options: You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/config/local.example.json)) to enable some convenience dev options:
* `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases. * `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases.
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode. * `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode.
@ -54,4 +65,4 @@ Edit config.json for configuration.
### Login methods ### Login methods
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations. ```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.

View file

@ -1,36 +1,36 @@
// https://github.com/shelljs/shelljs // https://github.com/shelljs/shelljs
require("./check-versions")(); require('./check-versions')()
require("shelljs/global"); require('shelljs/global')
env.NODE_ENV = "production"; env.NODE_ENV = 'production'
var path = require("path"); var path = require('path')
var config = require("../config"); var config = require('../config')
var webpack = require("webpack"); var ora = require('ora')
var webpackConfig = require("./webpack.prod.conf"); var webpack = require('webpack')
var webpackConfig = require('./webpack.prod.conf')
console.log( console.log(
" Tip:\n" + ' Tip:\n' +
" Built files are meant to be served over an HTTP server.\n" + ' Built files are meant to be served over an HTTP server.\n' +
" Opening index.html over file:// won't work.\n", ' Opening index.html over file:// won\'t work.\n'
); )
var assetsPath = path.join( var spinner = ora('building for production...')
config.build.assetsRoot, spinner.start()
config.build.assetsSubDirectory,
); var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
rm("-rf", assetsPath); rm('-rf', assetsPath)
mkdir("-p", assetsPath); mkdir('-p', assetsPath)
cp("-R", "static/*", assetsPath); cp('-R', 'static/*', assetsPath)
webpack(webpackConfig, function (err, stats) { webpack(webpackConfig, function (err, stats) {
if (err) throw err; spinner.stop()
process.stdout.write( if (err) throw err
stats.toString({ process.stdout.write(stats.toString({
colors: true, colors: true,
modules: false, modules: false,
children: false, children: false,
chunks: false, chunks: false,
chunkModules: false, chunkModules: false
}) + "\n", }) + '\n')
); })
});

View file

@ -5,7 +5,7 @@ var path = require('path')
var express = require('express') var express = require('express')
var webpack = require('webpack') var webpack = require('webpack')
var opn = require('opn') var opn = require('opn')
const { createProxyMiddleware } = require('http-proxy-middleware'); var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = process.env.NODE_ENV === 'testing' var webpackConfig = process.env.NODE_ENV === 'testing'
? require('./webpack.prod.conf') ? require('./webpack.prod.conf')
: require('./webpack.dev.conf') : require('./webpack.dev.conf')
@ -21,7 +21,6 @@ var compiler = webpack(webpackConfig)
var devMiddleware = require('webpack-dev-middleware')(compiler, { var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath, publicPath: webpackConfig.output.publicPath,
writeToDisk: true,
stats: { stats: {
colors: true, colors: true,
chunks: false chunks: false
@ -36,13 +35,7 @@ Object.keys(proxyTable).forEach(function (context) {
if (typeof options === 'string') { if (typeof options === 'string') {
options = { target: options } options = { target: options }
} }
const targetUrl = new URL(options.target); app.use(proxyMiddleware(context, options))
// add path
targetUrl.pathname = context;
options.target = targetUrl.toString();
console.log("Proxying", context, "to", options.target);
app.use(context, createProxyMiddleware(options))
}) })
// handle fallback for HTML5 history API // handle fallback for HTML5 history API

View file

@ -3,7 +3,6 @@ var config = require('../config')
var utils = require('./utils') var utils = require('./utils')
var projectRoot = path.resolve(__dirname, '../') var projectRoot = path.resolve(__dirname, '../')
var { VueLoaderPlugin } = require('vue-loader') var { VueLoaderPlugin } = require('vue-loader')
const ESLintPlugin = require('eslint-webpack-plugin');
var env = process.env.NODE_ENV var env = process.env.NODE_ENV
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the // check env & config/index.js to decide weither to enable CSS Sourcemaps for the
@ -36,7 +35,6 @@ module.exports = {
], ],
fallback: { fallback: {
"url": require.resolve("url/"), "url": require.resolve("url/"),
querystring: require.resolve("querystring-es3")
}, },
alias: { alias: {
'static': path.resolve(__dirname, '../static'), 'static': path.resolve(__dirname, '../static'),
@ -49,6 +47,20 @@ module.exports = {
module: { module: {
noParse: /node_modules\/localforage\/dist\/localforage.js/, noParse: /node_modules\/localforage\/dist\/localforage.js/,
rules: [ rules: [
{
enforce: 'pre',
test: /\.(js|vue)$/,
include: projectRoot,
exclude: /node_modules/,
use: {
loader: 'eslint-loader',
options: {
formatter: require('eslint-friendly-formatter'),
sourceMap: config.build.productionSourceMap,
extract: true
}
}
},
{ {
enforce: 'post', enforce: 'post',
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
@ -106,9 +118,6 @@ module.exports = {
] ]
}, },
plugins: [ plugins: [
new VueLoaderPlugin(), new VueLoaderPlugin()
new ESLintPlugin({
configType: 'flat'
})
] ]
} }

View file

@ -1,4 +1,4 @@
{ {
"target": "https://otp.akkoma.dev/", "target": "https://pleroma.soykaf.com/",
"staticConfigPreference": false "staticConfigPreference": false
} }

View file

@ -2,4 +2,5 @@ var { merge } = require('webpack-merge')
var devEnv = require('./dev.env') var devEnv = require('./dev.env')
module.exports = merge(devEnv, { module.exports = merge(devEnv, {
NODE_ENV: '"testing"'
}) })

View file

@ -1,15 +1,15 @@
# Akkoma-FE configuration and customization for instance administrators # Pleroma-FE configuration and customization for instance administrators
* *For user configuration, see [Akkoma-FE user guide](../user_guide)* * *For user configuration, see [Pleroma-FE user guide](../user_guide)*
* *For local development server configuration, see [Hacking, tweaking, contributing](HACKING.md)* * *For local development server configuration, see [Hacking, tweaking, contributing](HACKING.md)*
## Where configuration is stored ## Where configuration is stored
Akkoma-FE gets its configuration from several sources, in order of preference (the one above overrides ones below it) PleromaFE gets its configuration from several sources, in order of preference (the one above overrides ones below it)
1. `/api/statusnet/config.json` - this is generated on Backend and contains multiple things including instance name, char limit etc. It also contains FE/Client-specific data, Akkoma-FE uses `pleromafe` field of it. For more info on changing config on BE, look [here](https://docs.akkoma.dev/stable/configuration/cheatsheet.md#frontend_configurations) 1. `/api/statusnet/config.json` - this is generated on Backend and contains multiple things including instance name, char limit etc. It also contains FE/Client-specific data, PleromaFE uses `pleromafe` field of it. For more info on changing config on BE, look [here](https://docs.akkoma.dev/stable/configuration/cheatsheet.md#frontend_configurations)
2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://akkoma.dev/AkkomaGang/akkoma-fe/src/branch/develop/static/config.json). 2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/static/config.json).
3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://akkoma.dev/AkkomaGang/akkoma-fe/src/branch/develop/src/modules/instance.js) ) 3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/modules/instance.js) )
## Instance-defaults ## Instance-defaults
@ -59,7 +59,7 @@ Instance `logo`, could be any image, including svg. By default it assumes logo u
`logoMargin` allows you to adjust vertical margins between logo boundary and navbar borders. The idea is that to have logo's image without any extra margins and instead adjust them to your need in layout. `logoMargin` allows you to adjust vertical margins between logo boundary and navbar borders. The idea is that to have logo's image without any extra margins and instead adjust them to your need in layout.
### `minimalScopesMode` ### `minimalScopesMode`
Limit scope selection to *Direct*, *User default* and *Scope of post replying to*. This also makes it impossible to reply to a DM with a non-DM post from Akkoma-FE. Limit scope selection to *Direct*, *User default* and *Scope of post replying to*. This also makes it impossible to reply to a DM with a non-DM post from PleromaFE.
### `nsfwCensorImage` ### `nsfwCensorImage`
Use custom image for NSFW'd images Use custom image for NSFW'd images
@ -77,7 +77,7 @@ Change alignment of sidebar and panels to the right. Defaults to `false`.
Show panel showcasing instance features/settings to logged-out visitors Show panel showcasing instance features/settings to logged-out visitors
### `showInstanceSpecificPanel` ### `showInstanceSpecificPanel`
This allows you to include arbitrary HTML content in a panel below navigation menu. Akkoma-FE looks for an html page `instance/panel.html`, by default it's not provided in FE, but BE bundles some [default one](https://git.pleroma.social/pleroma/pleroma/blob/develop/priv/static/instance/panel.html). De-facto instance-defaults, since user can hide instance-specific panel. This allows you to include arbitrary HTML content in a panel below navigation menu. PleromaFE looks for an html page `instance/panel.html`, by default it's not provided in FE, but BE bundles some [default one](https://git.pleroma.social/pleroma/pleroma/blob/develop/priv/static/instance/panel.html). De-facto instance-defaults, since user can hide instance-specific panel.
### `subjectLineBehavior` ### `subjectLineBehavior`
How to handle subject line (CW) when replying to a post. How to handle subject line (CW) when replying to a post.

View file

@ -1,8 +1,8 @@
# Hacking, tweaking, contributing # Hacking, tweaking, contributing
## What Akkoma-FE even is, how it works ## What PleromaFE even is, how it works
Akkoma-FE is an SPA (Single-Page Application) backed by [Vue](https://vuejs.org/) framework. It means that it's just a nearly-empty HTML page with bunch of JavaScript that actually generates and controls DOM (i.e. html elements) in Runtime. Currently, there's no way around it - you have to have Javascript enabled in the browser to make it work, there is a theoretical possibility to generate some HTML server-side but it's not implemented yet. PleromaFE is an SPA (Single-Page Application) backed by [Vue](https://vuejs.org/) framework. It means that it's just a nearly-empty HTML page with bunch of JavaScript that actually generates and controls DOM (i.e. html elements) in Runtime. Currently, there's no way around it - you have to have Javascript enabled in the browser to make it work, there is a theoretical possibility to generate some HTML server-side but it's not implemented yet.
You can serve static html page and everything from any HTTP(S) server but currently it will try to access /api/ path at same domain it's running on, meaning that as of right now you cannot put it on one domain and access the other without proxying requests. You can serve static html page and everything from any HTTP(S) server but currently it will try to access /api/ path at same domain it's running on, meaning that as of right now you cannot put it on one domain and access the other without proxying requests.
@ -67,19 +67,19 @@ server {
### API, Data, Operations ### API, Data, Operations
In 99% cases Akkoma-FE uses [MastoAPI](https://docs.joinmastodon.org/api/) with [Pleroma Extensions](https://docs.akkoma.dev/stable/differences_in_mastoapi_responses.md) to fetch the data. The rest is either QvitterAPI leftovers or pleroma-exclusive APIs. QvitterAPI doesn't exactly have documentation and uses different JSON structure and sometimes different parameters and workflows, [this](https://twitter-api.readthedocs.io/en/latest/index.html) could be a good reference though. Some pleroma-exclusive API may still be using QvitterAPI JSON structure. In 99% cases PleromaFE uses [MastoAPI](https://docs.joinmastodon.org/api/) with [Pleroma Extensions](https://docs.akkoma.dev/stable/differences_in_mastoapi_responses.md) to fetch the data. The rest is either QvitterAPI leftovers or pleroma-exclusive APIs. QvitterAPI doesn't exactly have documentation and uses different JSON structure and sometimes different parameters and workflows, [this](https://twitter-api.readthedocs.io/en/latest/index.html) could be a good reference though. Some pleroma-exclusive API may still be using QvitterAPI JSON structure.
Akkoma-FE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://akkoma.dev/AkkomaGang/akkoma-fe/src/branch/develop/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation. PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation.
For most part, Akkoma-FE tries to store all the info it can get in global vuex store - every user and post are passed trough updating mechanism where data is either added or merged with existing data, reactively updating the information throughout UI, so if in newest request user's post counter increased, it will be instantly updated in open user profile cards. This is also used to find users, posts and sometimes to build timelines and/or request parameters. For most part, PleromaFE tries to store all the info it can get in global vuex store - every user and post are passed trough updating mechanism where data is either added or merged with existing data, reactively updating the information throughout UI, so if in newest request user's post counter increased, it will be instantly updated in open user profile cards. This is also used to find users, posts and sometimes to build timelines and/or request parameters.
Akkoma-FE also tries to persist this store, however only stable data is stored, such as user authentication and preferences, user highlights. Persistence is performed by saving and loading chunk of vuex store in browser's LocalStorage/IndexedDB. PleromaFE also tries to persist this store, however only stable data is stored, such as user authentication and preferences, user highlights. Persistence is performed by saving and loading chunk of vuex store in browser's LocalStorage/IndexedDB.
TODO: Refactor API code and document it here TODO: Refactor API code and document it here
### Themes ### Themes
Akkoma-FE uses custom theme "framework" which is pretty much just a style tag rendered by vue which only contains CSS3 variables. Every color used in UI should be derived from theme. Theme is stored in a JSON object containing color, opacity, shadow and font information, with most of it being optional. PleromaFE uses custom theme "framework" which is pretty much just a style tag rendered by vue which only contains CSS3 variables. Every color used in UI should be derived from theme. Theme is stored in a JSON object containing color, opacity, shadow and font information, with most of it being optional.
The most basic theme can consist of 4 to 8 "basic colors", which is also what previous version of themes allowed, with all other colors being derived from those basic colors, i.e. "light background" will be "background" color lightened/darkened, "panel header" will be same as "foreground". The idea is that you can specify just basic color palette and everything else will be generated automatically, but if you really need to tweak some specific color - you can. The most basic theme can consist of 4 to 8 "basic colors", which is also what previous version of themes allowed, with all other colors being derived from those basic colors, i.e. "light background" will be "background" color lightened/darkened, "panel header" will be same as "foreground". The idea is that you can specify just basic color palette and everything else will be generated automatically, but if you really need to tweak some specific color - you can.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 362.83 362.83">
<defs>
<style>
.cls-1, .cls-2 {
fill: #fff;
}
.cls-2 {
stroke: #fff;
stroke-miterlimit: 10;
}
</style>
</defs>
<g id="Layer_6" data-name="Layer 6">
<path class="cls-1" d="M115.2,131.89c6.26-6.54,20.19-20.63,42.39-26.14,15.79-3.92,28.51-1.28,33.51,0,83.72,21.41,116.03,201.78,77.79,226.32-10.28,6.6-26.86,2.7-36.77-3.3-32.63-19.78-29.3-72.87-44.44-73.73-5.11-.29-7.15,5.8-20.91,24.94-19.63,27.3-31.49,43.44-49.21,50.87-2.53,1.06-26.91,12.07-41.84,1.23-38.55-28-2.96-155.84,39.49-200.18Zm56.31,10.45c-27.39-.52-46.38,38.21-37.98,54.55,10.09,19.62,65.5,18.26,74.77-3.3,7.21-16.78-11.38-50.77-36.79-51.24Z"/>
</g>
<g id="Layer_4" data-name="Layer 4">
<path class="cls-1" d="M68.93,86.51c-6.55,27.74,252.45,113.97,267.56,89.66,9.24-14.87-64.9-83.62-163.53-97.57-39.06-5.52-100.95-5.14-104.03,7.91Z"/>
</g>
<g id="Layer_5" data-name="Layer 5">
<path class="cls-2" d="M138.96,93.76c.41-5.25,6.51-5.74,28.85-19.42,26.97-16.51,28.85-22.38,56.86-40.83,30.07-19.81,48.46-31.94,54.82-26.61,9.72,8.15-25.18,43.33-21.31,99.35,.87,12.61,3.12,17.79-.86,23.01-18.25,23.95-120.07-13.68-118.35-35.5Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

View file

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg4485"
width="512"
height="512"
viewBox="0 0 512 512"
sodipodi:docname="pleroma_logo_vector_nobg.svg"
inkscape:version="0.92.1 r15371">
<metadata
id="metadata4491">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4489" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="997"
id="namedview4487"
showgrid="false"
inkscape:zoom="0.70710678"
inkscape:cx="26.131594"
inkscape:cy="235.37499"
inkscape:window-x="1912"
inkscape:window-y="22"
inkscape:window-maximized="1"
inkscape:current-layer="svg4485" />
<g
id="g4612">
<path
sodipodi:nodetypes="cccccc"
inkscape:connector-curvature="0"
id="path4495"
d="M 235,89 V 423 H 152 V 115 l 26,-26 z"
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.17587938" />
<circle
r="26"
cx="178"
cy="115"
id="path4497"
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.17587938" />
<circle
r="26"
cx="335"
cy="230"
id="path4497-0"
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.17587938" />
<path
sodipodi:nodetypes="cccccc"
inkscape:connector-curvature="0"
id="path4516"
d="M 277,256 V 89 l 84,3e-6 L 361.00002,230 335,256 Z"
style="fill:#fba457;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
r="26"
cx="335"
cy="397"
id="path4497-0-6"
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.17587938" />
<path
sodipodi:nodetypes="cccccc"
inkscape:connector-curvature="0"
id="path4516-5"
d="m 277,423 v -83 h 84 l 2e-5,57 L 335,423 Z"
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -1,8 +1,8 @@
# Introduction to Akkoma-FE # Introduction to Pleroma-FE
## What is Akkoma-FE? ## What is Pleroma-FE?
Akkoma-FE is the default user-facing frontend for Pleroma. It's user interface is modeled after Qvitter which is modeled after an older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options. Pleroma-FE is the default user-facing frontend for Pleroma. It's user interface is modeled after Qvitter which is modeled after an older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options.
## How can I use it? ## How can I use it?
If your instance uses Akkoma-FE, you can acces it by going to your instance (e.g. <https://pleroma.soykaf.com>). You can read more about it's basic functionality in the [Akkoma-FE User Guide](./user_guide/). We also have [a guide for administrators](./CONFIGURATION.md) and for [hackers/contributors](./HACKING.md). If your instance uses Pleroma-FE, you can acces it by going to your instance (e.g. <https://pleroma.soykaf.com>). You can read more about it's basic functionality in the [Pleroma-FE User Guide](./user_guide/). We also have [a guide for administrators](./CONFIGURATION.md) and for [hackers/contributors](./HACKING.md).

View file

@ -1,13 +1,13 @@
# Adding stickers # Adding stickers
Akkoma-FE supports stickers, which are essentially little images stored server-side Pleroma-fe supports stickers, which are essentially little images stored server-side
which can be selected by a user to automatically attach them to a post. which can be selected by a user to automatically attach them to a post.
There's no explicit setting for these, they just rely on the existence of certain files. There's no explicit setting for these, they just rely on the existence of certain files.
## Initialising the sticker config file ## Initialising the sticker config file
You're probably serving Akkoma-FE from your instance's `instance/static/` directory - You're probably serving pleroma-fe from your instance's `instance/static/` directory -
this directy can also override files served at a given path. this directy can also override files served at a given path.
The first thing we need to do is set up our `stickers.json` file. At `instance/static/static/stickers.json`, The first thing we need to do is set up our `stickers.json` file. At `instance/static/static/stickers.json`,
@ -50,4 +50,4 @@ The `tabIcon` will appear on the sticker picker itself as a representative of th
You can add as many stickers as you like. They should all be in the same directory as your `pack.json`. You can add as many stickers as you like. They should all be in the same directory as your `pack.json`.
Now you should find that there's a sticky note icon on the emoji picker on Akkoma-FE that allows you to attach stickers. Now you should find that there's a sticky note icon on the emoji picker on pleroma-fe that allows you to attach stickers.

View file

@ -8,13 +8,13 @@
> >
> --Catbag > --Catbag
Akkoma-FE is the default user-facing frontend for Pleroma. If your instance uses Akkoma-FE, you can access it by going to your instance (e.g. <https://pleroma.soykaf.com>). After logging in you will have two columns in front of you. Here we're going to keep it to the default behaviour, but some instances swap the left and right columns. If you're on such an instance what we refer to as the left column will be on your right and vice versa. Pleroma-FE is the default user-facing frontend for Pleroma. If your instance uses Pleroma-FE, you can access it by going to your instance (e.g. <https://pleroma.soykaf.com>). After logging in you will have two columns in front of you. Here we're going to keep it to the default behaviour, but some instances swap the left and right columns. If you're on such an instance what we refer to as the left column will be on your right and vice versa.
### Left column ### Left column
- first block: This section is dedicated to [posting](posting_reading_basic_functions.md) - first block: This section is dedicated to [posting](posting_reading_basic_functions.md)
- second block: Here you can switch between the different views for the right column. - second block: Here you can switch between the different views for the right column.
- Optional third block: This is the Instance panel that can be activated, but is deactivated by default. It's fully customisable by instance admins and by default has links to the Akkoma-FE and Mastodon-FE. - Optional third block: This is the Instance panel that can be activated, but is deactivated by default. It's fully customisable by instance admins and by default has links to the Pleroma-FE and Mastodon-FE.
- fourth block: This is the Notifications block, here you will get notified whenever somebody mentions you, follows you, repeats or favorites one of your statuses - fourth block: This is the Notifications block, here you will get notified whenever somebody mentions you, follows you, repeats or favorites one of your statuses
### Right column ### Right column

View file

@ -15,13 +15,13 @@ Posts will contain the text you are posting, but some content will be modified:
Let's clear up some basic stuff. When you post something it's called a **post** or it could be called a **status** or even a **toot** or a **prööt** depending on whom you ask. Post has body/content but it also has some other stuff in it - from attachments, visibility scope, subject line... Let's clear up some basic stuff. When you post something it's called a **post** or it could be called a **status** or even a **toot** or a **prööt** depending on whom you ask. Post has body/content but it also has some other stuff in it - from attachments, visibility scope, subject line...
**Emoji** are small images embedded in text, there are two major types of emoji: [unicode emoji](https://en.wikipedia.org/wiki/Emoji) and custom emoji. While unicode emoji are universal and standardized, they can appear differently depending on where you are using them or may not appear at all on older systems. Custom emoji are a more *fun* kind - instance administrator can define many images as *custom emoji* for their users. This works very simple - custom emoji is defined by its *shortcode* and an image, so that any shortcode enclosed in colons get replaced with image if such shortcode exist. **Emoji** are small images embedded in text, there are two major types of emoji: [unicode emoji](https://en.wikipedia.org/wiki/Emoji) and custom emoji. While unicode emoji are universal and standardized, they can appear differently depending on where you are using them or may not appear at all on older systems. Custom emoji are a more *fun* kind - instance administrator can define many images as *custom emoji* for their users. This works very simple - custom emoji is defined by its *shortcode* and an image, so that any shortcode enclosed in colons get replaced with image if such shortcode exist.
Let's say there's a `:akkoma:` emoji defined on an instance. That means Let's say there's a `:pleroma:` emoji defined on an instance. That means
> First time using :akkoma: akkoma! > First time using :pleroma: pleroma!
will become will become
> First time using ![akkoma](../assets/example_emoji.png) akkoma! > First time using ![pleroma](../assets/example_emoji.png) pleroma!
Note that you can only use emoji defined on your instance, you cannot "copy" someone else's emoji, and will have to ask your administrator to copy emoji from other instance to yours. Note that you can only use emoji defined on your instance, you cannot "copy" someone else's emoji, and will have to ask your administrator to copy emoji from other instance to yours.
Lastly, there's two convenience options for emoji: an emoji picker (smiley face to the right of "submit" button) and autocomplete suggestions - when you start typing :shortcode: it will automatically try to suggest you emoji and complete the shortcode for you if you select one. If emoji doesn't show up in suggestions nor in emoji picker it means there's no such emoji on your instance, if shortcode doesn't match any defined emoji it will appear as text. Lastly, there's two convenience options for emoji: an emoji picker (smiley face to the right of "submit" button) and autocomplete suggestions - when you start typing :shortcode: it will automatically try to suggest you emoji and complete the shortcode for you if you select one. If emoji doesn't show up in suggestions nor in emoji picker it means there's no such emoji on your instance, if shortcode doesn't match any defined emoji it will appear as text.
**Attachments** are fairly simple - you can attach any file to a post as long as the file is within maximum size limits. If you're uploading explicit material you can mark all of your attachments as sensitive (or add the `#nsfw` tag) - it will hide the images and videos behind a warning so that it won't be displayed instantly. **Attachments** are fairly simple - you can attach any file to a post as long as the file is within maximum size limits. If you're uploading explicit material you can mark all of your attachments as sensitive (or add the `#nsfw` tag) - it will hide the images and videos behind a warning so that it won't be displayed instantly.
@ -42,7 +42,7 @@ A few things to consider about the security and usage of these scopes:
- Changing scopes during a thread or adding people to a direct message will not retroactively make them see the whole conversation. If you add someone to a direct message conversation, they will not see the post that happened before they were mentioned. - Changing scopes during a thread or adding people to a direct message will not retroactively make them see the whole conversation. If you add someone to a direct message conversation, they will not see the post that happened before they were mentioned.
* **Reply-to** if you are replying to someone, your post will also contain a note that your post is referring to the post you're replying to. Person you're replying to will receive a notification *even* if you remove them from mentioned people. You won't receive notifications when replying to your own posts, but it's useful to reply to your own posts to provide people some context if it's a follow-up to a previous post. There's a small "Reply to ..." label under post author's name which you can hover on to see what post it's referring to. * **Reply-to** if you are replying to someone, your post will also contain a note that your post is referring to the post you're replying to. Person you're replying to will receive a notification *even* if you remove them from mentioned people. You won't receive notifications when replying to your own posts, but it's useful to reply to your own posts to provide people some context if it's a follow-up to a previous post. There's a small "Reply to ..." label under post author's name which you can hover on to see what post it's referring to.
Sometimes you may encounter posts that seem different than what they are supposed to. For example, you might see a direct message without any mentions in the text. This can happen because internally, the Fediverse has a different addressing mechanism similar to email, with `to` and `cc` fields. While these are not directly accessible in Akkoma-FE, other software in the Fediverse might generate those posts. Do not worry in these cases, these are normal and not a bug. Sometimes you may encounter posts that seem different than what they are supposed to. For example, you might see a direct message without any mentions in the text. This can happen because internally, the Fediverse has a different addressing mechanism similar to email, with `to` and `cc` fields. While these are not directly accessible in PleromaFE, other software in the Fediverse might generate those posts. Do not worry in these cases, these are normal and not a bug.
## Rich text ## Rich text
@ -51,7 +51,7 @@ By default new posts you make are plaintext, meaning you can't make text **bold*
Here is a small example of some text in markdown. Here is a small example of some text in markdown.
``` ```
This is an example of markdown text using **bold** and *cursive* text. This is an example of markdown text using **bold** and *cursive* text.
To get a newline we add two spaces at the end of the previous line. To get a newline we add two spaces at the end of the previous line.
Let's also add a list Let's also add a list
@ -67,7 +67,7 @@ If you set the input-method to Markdown, and post this, it will look something l
## Misskey markdown ## Misskey markdown
Akkoma-FE includes support for writing and rendering The akkoma version of pleroma-fe includes support for writing and rendering
misskey markdown (MFM). To write this you will need to select "MFM" from misskey markdown (MFM). To write this you will need to select "MFM" from
the content type dropdown (if supported), and then you can format text the content type dropdown (if supported), and then you can format text
[in MFM](https://akkoma.dev/sfr/marked-mfm/src/branch/master/docs/syntax.md). [in MFM](https://akkoma.dev/sfr/marked-mfm/src/branch/master/docs/syntax.md).

View file

@ -83,7 +83,7 @@ Here you can change your password, revoke access tokens, configure 2-factor auth
## Theme ## Theme
Here you can change the look and feel of Akkoma-FE. You can choose from several instance-provided presets and you can load one from file and save current theme to file. Before you apply new theme you can see what it will look like approximately in preview section. Here you can change the look and feel of Pleroma-FE. You can choose from several instance-provided presets and you can load one from file and save current theme to file. Before you apply new theme you can see what it will look like approximately in preview section.
The themes engine was made to be easy to use while giving an option for powerful in-depth customization - you can just tweak colors on "Common" tab and leave everything else as is. The themes engine was made to be easy to use while giving an option for powerful in-depth customization - you can just tweak colors on "Common" tab and leave everything else as is.

View file

@ -4,7 +4,7 @@ When you see someone, you can click on their user picture to view their profile,
**Following** is self-explanatory, it adds them to your Home Timeline, lists you as a follower and gives you access to follower-only posts if they have any. **Following** is self-explanatory, it adds them to your Home Timeline, lists you as a follower and gives you access to follower-only posts if they have any.
**Muting** collapses posts and notifications made by them, giving you an option to see the post if you're curious. Clients other than Akkoma-FE may completely remove their posts. **Muting** collapses posts and notifications made by them, giving you an option to see the post if you're curious. Clients other than PleromaFE may completely remove their posts.
**Blocking** a user removes them from your timeline and notifications and prevents them from following you (automatically unfollows them from you). **Blocking** a user removes them from your timeline and notifications and prevents them from following you (automatically unfollows them from you).

View file

@ -1,11 +1,11 @@
site_name: Akkoma-FE Documentation site_name: Pleroma-FE Documentation
theme: theme:
favicon: 'images/akkoma_logo_vector_bg_32.png' favicon: 'images/pleroma_logo_vector_bg_32.png'
name: 'material' name: 'material'
custom_dir: 'theme' custom_dir: 'theme'
# Disable google fonts # Disable google fonts
font: false font: false
logo: 'images/akkoma_logo_vector_nobg.svg' logo: 'images/pleroma_logo_vector_nobg.svg'
features: features:
- tabs - tabs
palette: palette:
@ -14,8 +14,8 @@ theme:
extra_css: extra_css:
- css/extra.css - css/extra.css
repo_name: 'AkkomaGang/akkoma-fe' repo_name: 'AkkomaGang/pleroma-fe'
repo_url: 'https://akkoma.dev/AkkomaGang/akkoma-fe' repo_url: 'https://akkoma.dev/AkkomaGang/pleroma-fe'
extra: extra:
repo_icon: gitea repo_icon: gitea

View file

@ -38,11 +38,11 @@
{% endif %} {% endif %}
{% if page and page.url.startswith('backend') %} {% if page and page.url.startswith('backend') %}
{% set repo_url = "https://akkoma.dev/AkkomaGang/akkoma" %} {% set repo_url = "https://git.pleroma.social/pleroma/pleroma" %}
{% set repo_name = "AkkomaGang/akkoma" %} {% set repo_name = "pleroma/pleroma" %}
{% elif page and page.url.startswith('frontend') %} {% elif page and page.url.startswith('frontend') %}
{% set repo_url = "https://akkoma.dev/AkkomaGang/akkoma-fe" %} {% set repo_url = "https://git.pleroma.social/pleroma/pleroma-fe" %}
{% set repo_name = "AkkomaGang/akkoma-fe" %} {% set repo_name = "pleroma/pleroma-fe" %}
{% else %} {% else %}
{% set repo_url = config.repo_url %} {% set repo_url = config.repo_url %}
{% set repo_name = config.repo_name %} {% set repo_name = config.repo_name %}

View file

@ -1,31 +0,0 @@
const pluginVue = require('eslint-plugin-vue')
const pluginImport = require('eslint-plugin-import')
module.exports = [
...pluginVue.configs['flat/recommended'],
{
languageOptions: {
parserOptions: {
parser: '@babel/eslint-parser',
sourceType: 'module'
}
},
rules: {
// allow paren-less arrow functions
'arrow-parens': 0,
// allow async-await
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'vue/require-prop-types': 0,
'vue/no-unused-vars': 0,
'no-tabs': 0,
'vue/multi-word-component-names': 0,
'vue/no-reserved-component-names': 0
},
ignores: [
'build/*.js',
'config/*.js'
]
}
]

View file

@ -18,5 +18,6 @@
<div id="app"></div> <div id="app"></div>
<div id="modal"></div> <div id="modal"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
<script type="text/javascript" src="/instance/pleroma-mod-loader.js"></script>
</body> </body>
</html> </html>

View file

@ -1,6 +1,6 @@
{ {
"name": "pleroma_fe", "name": "pleroma_fe",
"version": "3.10.0", "version": "3.5.0",
"description": "A frontend for Akkoma instances", "description": "A frontend for Akkoma instances",
"author": "Roger Braun <roger@rogerbraun.net>", "author": "Roger Braun <roger@rogerbraun.net>",
"private": true, "private": true,
@ -12,118 +12,120 @@
"e2e": "node test/e2e/runner.js", "e2e": "node test/e2e/runner.js",
"test": "npm run unit && npm run e2e", "test": "npm run unit && npm run e2e",
"stylelint": "stylelint src/**/*.scss", "stylelint": "stylelint src/**/*.scss",
"lint": "eslint src test/unit/specs test/e2e/specs", "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
"lint-fix": "eslint --fix src test/unit/specs test/e2e/specs" "lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.17.8", "@babel/runtime": "7.17.8",
"@chenfengyuan/vue-qrcode": "^2.0.0", "@chenfengyuan/vue-qrcode": "2.0.0",
"@floatingghost/pinch-zoom-element": "^1.3.1", "@floatingghost/pinch-zoom-element": "^1.3.1",
"@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-regular-svg-icons": "^6.5.2", "@fortawesome/free-regular-svg-icons": "^6.1.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/vue-fontawesome": "^3.0.8", "@fortawesome/vue-fontawesome": "3.0.1",
"@vuelidate/core": "^2.0.3", "@vuelidate/core": "^2.0.0",
"@vuelidate/validators": "^2.0.4", "@vuelidate/validators": "^2.0.0",
"blurhash": "^2.0.5", "blurhash": "^2.0.4",
"body-scroll-lock": "^3.1.5", "body-scroll-lock": "2.7.1",
"chromatism": "^3.0.0", "chromatism": "3.0.0",
"click-outside-vue3": "^4.0.1", "click-outside-vue3": "4.0.1",
"cropperjs": "^1.6.2", "cropperjs": "1.5.12",
"diff": "^5.2.0", "diff": "3.5.0",
"escape-html": "^1.0.3", "escape-html": "1.0.3",
"iso-639-1": "^2.1.15", "iso-639-1": "^2.1.15",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"localforage": "^1.10.0", "localforage": "1.10.0",
"parse-link-header": "^2.0.0", "parse-link-header": "^2.0.0",
"phoenix": "^1.7.12", "phoenix": "1.6.2",
"punycode.js": "^2.3.1", "punycode.js": "2.1.0",
"qrcode": "^1.5.3", "qrcode": "1",
"querystring-es3": "^0.2.1", "url": "^0.11.0",
"url": "^0.11.3", "vue": "^3.2.31",
"vue": "^3.4.38", "vue-i18n": "^9.2.2",
"vue-i18n": "^9.14.0", "vue-router": "4.0.14",
"vue-router": "^4.4.3", "vue-template-compiler": "2.6.11",
"vue-template-compiler": "^2.7.16", "vuex": "4.0.2"
"vuex": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.24.6", "@babel/core": "7.17.8",
"@babel/eslint-parser": "^7.19.1", "@babel/eslint-parser": "^7.19.1",
"@babel/plugin-transform-runtime": "^7.24.6", "@babel/plugin-transform-runtime": "7.17.0",
"@babel/preset-env": "^7.24.6", "@babel/preset-env": "7.16.11",
"@babel/register": "^7.24.6", "@babel/register": "7.17.7",
"@intlify/vue-i18n-loader": "^5.0.0", "@intlify/vue-i18n-loader": "^5.0.0",
"@ungap/event-target": "^0.2.4", "@ungap/event-target": "0.2.3",
"@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", "@vue/babel-helper-vue-jsx-merge-props": "1.2.1",
"@vue/babel-plugin-jsx": "^1.2.2", "@vue/babel-plugin-jsx": "1.1.1",
"@vue/compiler-sfc": "^3.1.0", "@vue/compiler-sfc": "^3.1.0",
"@vue/test-utils": "^2.0.2", "@vue/test-utils": "^2.0.2",
"autoprefixer": "^10.4.19", "autoprefixer": "6.7.7",
"babel-loader": "^9.1.0", "babel-loader": "^9.1.0",
"babel-plugin-lodash": "^3.3.4", "babel-plugin-lodash": "3.3.4",
"chai": "^4.3.7", "chai": "^4.3.7",
"chalk": "^1.1.3", "chalk": "1.1.3",
"chromedriver": "^119.0.1", "chromedriver": "^107.0.3",
"connect-history-api-fallback": "^2.0.0", "connect-history-api-fallback": "^2.0.0",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"css-loader": "^7.1.2", "css-loader": "^6.7.2",
"custom-event-polyfill": "^1.0.7", "custom-event-polyfill": "^1.0.7",
"eslint": "^9.3.0", "eslint": "^7.32.0",
"eslint-config-standard": "^17.1.0", "eslint-config-standard": "^17.0.0",
"eslint-friendly-formatter": "^4.0.1", "eslint-friendly-formatter": "^4.0.1",
"eslint-plugin-import": "^2.29.1", "eslint-loader": "^4.0.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.2.0", "eslint-plugin-promise": "^6.1.1",
"eslint-plugin-standard": "^5.0.0", "eslint-plugin-standard": "^5.0.0",
"eslint-plugin-vue": "^9.26.0", "eslint-plugin-vue": "^9.7.0",
"eslint-webpack-plugin": "^4.2.0", "eventsource-polyfill": "0.9.6",
"eventsource-polyfill": "^0.9.6", "express": "4.17.3",
"express": "^4.19.2",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"function-bind": "^1.1.2", "function-bind": "1.1.1",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"http-proxy-middleware": "^3.0.0", "http-proxy-middleware": "0.21.0",
"json-loader": "^0.5.7", "inject-loader": "2.0.1",
"karma": "^6.4.3", "isparta-loader": "2.0.0",
"karma-coverage": "^2.2.1", "json-loader": "0.5.7",
"karma-firefox-launcher": "^2.1.3", "karma": "6.3.17",
"karma-mocha": "^2.0.1", "karma-coverage": "1.1.2",
"karma-mocha-reporter": "^2.2.5", "karma-firefox-launcher": "1.3.0",
"karma-sinon-chai": "^2.0.2", "karma-mocha": "2.0.1",
"karma-sourcemap-loader": "^0.4.0", "karma-mocha-reporter": "2.2.5",
"karma-spec-reporter": "^0.0.36", "karma-sinon-chai": "2.0.2",
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.33",
"karma-webpack": "^5.0.0", "karma-webpack": "^5.0.0",
"lodash": "^4.17.21", "lodash": "4.17.21",
"lolex": "^6.0.0", "lolex": "1.6.0",
"mini-css-extract-plugin": "^2.9.0", "mini-css-extract-plugin": "0.12.0",
"mocha": "^10.4.0", "mocha": "3.5.3",
"nightwatch": "^3.6.3", "nightwatch": "0.9.21",
"opn": "^6.0.0", "opn": "4.0.2",
"ora": "0.4.1",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"postcss-loader": "^8.1.1", "postcss-loader": "3.0.0",
"postcss-sass": "^0.5.0", "postcss-sass": "^0.5.0",
"raw-loader": "^4.0.2", "raw-loader": "0.5.1",
"sass": "^1.77.2", "sass": "^1.56.0",
"sass-loader": "^14.2.1", "sass-loader": "^13.2.0",
"selenium-server": "^3.141.59", "selenium-server": "2.53.1",
"semver": "^7.6.2", "semver": "5.7.1",
"shelljs": "^0.8.5", "shelljs": "0.8.5",
"sinon": "^18.0.0", "sinon": "2.4.1",
"sinon-chai": "^3.7.0", "sinon-chai": "2.14.0",
"stylelint": "^14.15.0", "stylelint": "^14.15.0",
"stylelint-config-recommended-vue": "^1.4.0", "stylelint-config-recommended-vue": "^1.4.0",
"stylelint-config-standard": "^29.0.0", "stylelint-config-standard": "^29.0.0",
"stylelint-config-standard-scss": "^6.1.0", "stylelint-config-standard-scss": "^6.1.0",
"stylelint-rscss": "^0.4.0", "stylelint-rscss": "^0.4.0",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"vue-loader": "^17.4.2", "vue-loader": "^17.0.0",
"vue-style-loader": "^4.1.3", "vue-style-loader": "^4.1.2",
"webpack": "^5.91.0", "webpack": "^5.75.0",
"webpack-dev-middleware": "^7.2.1", "webpack-dev-middleware": "^5.3.3",
"webpack-hot-middleware": "^2.26.1", "webpack-hot-middleware": "^2.25.1",
"webpack-merge": "^5.10.0", "webpack-merge": "^5.8.0",
"workbox-webpack-plugin": "^7.1.0" "workbox-webpack-plugin": "^6.5.4"
}, },
"engines": { "engines": {
"node": ">= 16.0.0", "node": ">= 16.0.0",

View file

@ -64,11 +64,6 @@ export default {
'-' + this.layoutType '-' + this.layoutType
] ]
}, },
pageBackground () {
return this.mergedConfig.displayPageBackgrounds
? this.$store.state.users.displayBackground
: null
},
currentUser () { return this.$store.state.users.currentUser }, currentUser () { return this.$store.state.users.currentUser },
userBackground () { return this.currentUser.background_image }, userBackground () { return this.currentUser.background_image },
instanceBackground () { instanceBackground () {
@ -76,7 +71,7 @@ export default {
? null ? null
: this.$store.state.instance.background : this.$store.state.instance.background
}, },
background () { return this.pageBackground || this.userBackground || this.instanceBackground }, background () { return this.userBackground || this.instanceBackground },
bgStyle () { bgStyle () {
if (this.background) { if (this.background) {
return { return {

View file

@ -8,7 +8,7 @@
} }
html { html {
font-size: 0.875rem; font-size: 14px;
// overflow-x: clip causes my browser's tab to crash with SIGILL lul // overflow-x: clip causes my browser's tab to crash with SIGILL lul
} }
@ -469,7 +469,7 @@ textarea,
color: $fallback--lightText; color: $fallback--lightText;
color: var(--inputText, $fallback--lightText); color: var(--inputText, $fallback--lightText);
font-family: sans-serif; font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif); font-family: var(--inputFont, sans-serif);
font-size: 1em; font-size: 1em;
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;

View file

@ -18,6 +18,8 @@ import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
import { applyTheme } from '../services/style_setter/style_setter.js' import { applyTheme } from '../services/style_setter/style_setter.js'
import FaviconService from '../services/favicon_service/favicon_service.js' import FaviconService from '../services/favicon_service/favicon_service.js'
import DomNodeToComponent from '../modules/domNodeToComponent.js'
let staticInitialResults = null let staticInitialResults = null
const parsedInitialResults = () => { const parsedInitialResults = () => {
@ -422,6 +424,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
app.use(vClickOutside) app.use(vClickOutside)
app.use(VBodyScrollLock) app.use(VBodyScrollLock)
app.use(DomNodeToComponent)
app.component('FAIcon', FontAwesomeIcon) app.component('FAIcon', FontAwesomeIcon)
app.component('FALayers', FontAwesomeLayers) app.component('FALayers', FontAwesomeLayers)

View file

@ -9,7 +9,7 @@
</div> </div>
</template> </template>
<script src="./about.js"></script> <script src="./about.js" ></script>
<style lang="scss"> <style lang="scss">
</style> </style>

View file

@ -6,7 +6,7 @@
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
remove-padding remove-padding
> >
<template #content> <template v-slot:content>
<div class="dropdown-menu"> <div class="dropdown-menu">
<template v-if="relationship.following"> <template v-if="relationship.following">
<button <button
@ -71,7 +71,7 @@
</button> </button>
</div> </div>
</template> </template>
<template #trigger> <template v-slot:trigger>
<button class="button-unstyled ellipsis-button"> <button class="button-unstyled ellipsis-button">
<FAIcon <FAIcon
class="icon" class="icon"
@ -93,7 +93,7 @@
keypath="user_card.block_confirm" keypath="user_card.block_confirm"
tag="span" tag="span"
> >
<template #user> <template v-slot:user>
<span <span
v-text="user.screen_name_ui" v-text="user.screen_name_ui"
/> />

View file

@ -1,5 +1,4 @@
import StillImage from '../still-image/still-image.vue' import StillImage from '../still-image/still-image.vue'
import Flash from '../flash/flash.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue' import VideoAttachment from '../video_attachment/video_attachment.vue'
import nsfwImage from '../../assets/nsfw.png' import nsfwImage from '../../assets/nsfw.png'
import fileTypeService from '../../services/file_type/file_type.service.js' import fileTypeService from '../../services/file_type/file_type.service.js'
@ -62,7 +61,6 @@ const Attachment = {
} }
}, },
components: { components: {
Flash,
StillImage, StillImage,
VideoAttachment, VideoAttachment,
Blurhash Blurhash

View file

@ -37,7 +37,7 @@
white-space: pre-line; white-space: pre-line;
word-break: break-word; word-break: break-word;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: auto; overflow: scroll;
} }
&.-static { &.-static {

View file

@ -246,8 +246,8 @@
ref="flash" ref="flash"
class="flash" class="flash"
:src="attachment.large_thumb_url || attachment.url" :src="attachment.large_thumb_url || attachment.url"
@player-opened="setFlashLoaded(true)" @playerOpened="setFlashLoaded(true)"
@player-closed="setFlashLoaded(false)" @playerClosed="setFlashLoaded(false)"
/> />
</span> </span>
</div> </div>

View file

@ -14,7 +14,7 @@
</div> </div>
</template> </template>
<script src="./avatar_list.js"></script> <script src="./avatar_list.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -0,0 +1,108 @@
const ChatLayout = {
methods: {
setChatLayout () {
let body = document.querySelector('body')
if (body) {
body.style.overscrollBehavior = 'none'
}
if (this.isMobileLayout) {
this.setMobileChatLayout()
}
},
unsetChatLayout () {
this.unsetMobileChatLayout()
let body = document.querySelector('body')
if (body) {
body.style.overscrollBehavior = 'unset'
}
},
setMobileChatLayout () {
// This is a hacky way to adjust the global layout to the mobile chat (without modifying the rest of the app).
// This layout prevents empty spaces from being visible at the bottom
// of the chat on iOS Safari (`safe-area-inset`) when
// - the on-screen keyboard appears and the user starts typing
// - the user selects the text inside the input area
// - the user selects and deletes the text that is multiple lines long
// TODO: unify the chat layout with the global layout.
let html = document.querySelector('html')
if (html) {
html.style.overflow = 'hidden'
html.style.height = '100%'
}
let body = document.querySelector('body')
if (body) {
body.style.height = '100%'
}
let app = document.getElementById('app')
if (app) {
app.style.height = '100%'
app.style.overflow = 'hidden'
app.style.minHeight = 'auto'
}
let appBgWrapper = window.document.getElementById('app_bg_wrapper')
if (appBgWrapper) {
appBgWrapper.style.overflow = 'hidden'
}
let main = document.getElementsByClassName('main')[0]
if (main) {
main.style.overflow = 'hidden'
main.style.height = '100%'
}
let content = document.getElementById('content')
if (content) {
content.style.paddingTop = '0'
content.style.height = '100%'
content.style.overflow = 'visible'
}
this.$nextTick(() => {
this.updateSize()
})
},
unsetMobileChatLayout () {
let html = document.querySelector('html')
if (html) {
html.style.overflow = 'visible'
html.style.height = 'unset'
}
let body = document.querySelector('body')
if (body) {
body.style.height = 'unset'
}
let app = document.getElementById('app')
if (app) {
app.style.height = '100%'
app.style.overflow = 'visible'
app.style.minHeight = '100vh'
}
let appBgWrapper = document.getElementById('app_bg_wrapper')
if (appBgWrapper) {
appBgWrapper.style.overflow = 'visible'
}
let main = document.getElementsByClassName('main')[0]
if (main) {
main.style.overflow = 'visible'
main.style.height = 'unset'
}
let content = document.getElementById('content')
if (content) {
content.style.paddingTop = '60px'
content.style.height = 'unset'
content.style.overflow = 'unset'
}
}
}
}
export default ChatLayout

View file

@ -0,0 +1,34 @@
import StillImage from '../still-image/still-image.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapState } from 'vuex'
const ChatAvatar = {
props: ['users', 'fallbackUser', 'width', 'height'],
components: {
StillImage
},
methods: {
getUserProfileLink (user) {
return generateProfileLink(user.id, user.screen_name)
}
},
computed: {
firstUser () {
return this.users[0] || this.fallbackUser
},
secondUser () {
return this.users[1]
},
thirdUser () {
return this.users[2]
},
fourthUser () {
return this.users[3]
},
...mapState({
betterShadow: state => state.interface.browserSupport.cssFilter
})
}
}
export default ChatAvatar

View file

@ -0,0 +1,130 @@
<template>
<div
v-if="firstUser && secondUser"
class="direct-conversation-multi-user-avatar"
:style="{ 'width': width, 'height': height }"
>
<StillImage
v-if="fourthUser"
class="avatar avatar-fourth direct-conversation-avatar"
:alt="fourthUser.screen_name"
:title="fourthUser.screen_name"
:src="fourthUser.profile_image_url_original"
error-src="/images/avi.png"
:class="{ 'better-shadow': betterShadow }"
/>
<StillImage
v-if="thirdUser"
class="avatar avatar-third direct-conversation-avatar"
:alt="thirdUser.screen_name"
:title="thirdUser.screen_name"
:src="thirdUser.profile_image_url_original"
error-src="/images/avi.png"
:class="{ 'better-shadow': betterShadow }"
/>
<StillImage
class="avatar avatar-second direct-conversation-avatar"
:alt="secondUser.screen_name"
:title="secondUser.screen_name"
:src="secondUser.profile_image_url_original"
error-src="/images/avi.png"
:class="{ 'better-shadow': betterShadow }"
:style="{ 'height': fourthUser ? '50%' : '100%' }"
/>
<StillImage
class="avatar avatar-first direct-conversation-avatar"
:alt="firstUser.screen_name"
:title="firstUser.screen_name"
:src="firstUser.profile_image_url_original"
error-src="/images/avi.png"
:class="{ 'better-shadow': betterShadow }"
:style="{ 'height': thirdUser ? '50%' : '100%' }"
/>
</div>
<router-link
v-else
:to="getUserProfileLink(firstUser)"
>
<StillImage
:style="{ 'width': width, 'height': height }"
class="avatar direct-conversation-avatar single-user"
:alt="firstUser.screen_name"
:title="firstUser.screen_name"
:src="firstUser.profile_image_url_original"
error-src="/images/avi.png"
:class="{ 'better-shadow': betterShadow }"
/>
</router-link>
</template>
<script src="./chat_avatar.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.direct-conversation-multi-user-avatar {
position: relative;
cursor: pointer;
width: 48px;
height: 48px;
border-radius: 50%;
overflow: hidden;
.avatar.still-image {
width: 50%;
height: 50%;
border-radius: 0;
img, canvas {
object-fit: cover;
}
&.avatar-first {
float: right;
position: absolute;
bottom: 0;
}
&.avatar-second {
float: right;
}
&.avatar-third {
float: right;
position: absolute;
}
&.avatar-fourth {
float: right;
position: absolute;
bottom: 0;
right: 0;
}
}
}
.direct-conversation-avatar {
display: inline-block;
vertical-align: middle;
&.single-user {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
}
.avatar.still-image {
width: 48px;
height: 48px;
box-shadow: var(--avatarStatusShadow);
border-radius: 0;
&.better-shadow {
box-shadow: var(--avatarStatusShadowInset);
filter: var(--avatarStatusShadowFilter)
}
&.animated::before {
display: none;
}
}
}
</style>

View file

@ -18,7 +18,7 @@ export default {
if (this.date.getTime() === today.getTime()) { if (this.date.getTime() === today.getTime()) {
return this.$t('display_date.today') return this.$t('display_date.today')
} else { } else {
return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' }) return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale.split('_')[0]), { day: 'numeric', month: 'long' })
} }
} }
} }

View file

@ -22,12 +22,12 @@
<script> <script>
export default { export default {
emits: ['update:modelValue'],
props: [ props: [
'modelValue', 'modelValue',
'indeterminate', 'indeterminate',
'disabled' 'disabled'
], ]
emits: ['update:modelValue']
} }
</script> </script>

View file

@ -14,7 +14,7 @@
:model-value="present" :model-value="present"
:disabled="disabled" :disabled="disabled"
class="opt" class="opt"
@update:model-value="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" @update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
/> />
<div class="input color-input-field"> <div class="input color-input-field">
<input <input
@ -46,6 +46,7 @@
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" src="./color_input.scss"></style>
<script> <script>
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js' import { hex2rgb } from '../../services/color_convert/color_convert.js'
@ -107,7 +108,6 @@ export default {
} }
} }
</script> </script>
<style lang="scss" src="./color_input.scss"></style>
<style lang="scss"> <style lang="scss">
.color-control { .color-control {

View file

@ -25,8 +25,6 @@
</dialog-modal> </dialog-modal>
</template> </template>
<script src="./confirm_modal.js"></script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../_variables'; @import '../../_variables';
@ -37,3 +35,5 @@
} }
} }
</style> </style>
<script src="./confirm_modal.js"></script>

View file

@ -267,11 +267,11 @@ const conversation = {
}, },
replies () { replies () {
let i = 1 let i = 1
// eslint-disable-next-line camelcase
return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => { return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => {
/* eslint-disable camelcase */
const irid = in_reply_to_status_id const irid = in_reply_to_status_id
/* eslint-enable camelcase */
if (irid) { if (irid) {
result[irid] = result[irid] || [] result[irid] = result[irid] || []
result[irid].push({ result[irid].push({

View file

@ -91,7 +91,7 @@
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)" :controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
@goto="setHighlight" @goto="setHighlight"
@toggle-expanded="toggleExpanded" @toggleExpanded="toggleExpanded"
/> />
<div <div
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1" v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
@ -184,7 +184,7 @@
:toggle-status-content-property="toggleStatusContentProperty" :toggle-status-content-property="toggleStatusContentProperty"
@goto="setHighlight" @goto="setHighlight"
@toggle-expanded="toggleExpanded" @toggleExpanded="toggleExpanded"
/> />
</div> </div>
</div> </div>

View file

@ -44,9 +44,9 @@
/> />
</router-link> </router-link>
<router-link <router-link
v-if="publicTimelineVisible"
:to="{ name: 'public-timeline' }" :to="{ name: 'public-timeline' }"
class="nav-icon" class="nav-icon"
v-if="publicTimelineVisible"
> >
<FAIcon <FAIcon
fixed-width fixed-width
@ -68,9 +68,9 @@
/> />
</router-link> </router-link>
<router-link <router-link
v-if="federatedTimelineVisible"
:to="{ name: 'public-external-timeline' }" :to="{ name: 'public-external-timeline' }"
class="nav-icon" class="nav-icon"
v-if="federatedTimelineVisible"
> >
<FAIcon <FAIcon
fixed-width fixed-width

View file

@ -9,7 +9,7 @@
class="btn button-default" class="btn button-default"
> >
{{ $t('domain_mute_card.unmute') }} {{ $t('domain_mute_card.unmute') }}
<template #progress> <template v-slot:progress>
{{ $t('domain_mute_card.unmute_progress') }} {{ $t('domain_mute_card.unmute_progress') }}
</template> </template>
</ProgressButton> </ProgressButton>
@ -19,7 +19,7 @@
class="btn button-default" class="btn button-default"
> >
{{ $t('domain_mute_card.mute') }} {{ $t('domain_mute_card.mute') }}
<template #progress> <template v-slot:progress>
{{ $t('domain_mute_card.mute_progress') }} {{ $t('domain_mute_card.mute_progress') }}
</template> </template>
</ProgressButton> </ProgressButton>

View file

@ -2,7 +2,7 @@
<Modal <Modal
v-if="isFormVisible" v-if="isFormVisible"
class="edit-form-modal-view" class="edit-form-modal-view"
@backdrop-clicked="closeModal" @backdropClicked="closeModal"
> >
<div class="edit-form-modal-panel panel"> <div class="edit-form-modal-panel panel">
<div class="panel-heading"> <div class="panel-heading">
@ -11,10 +11,10 @@
<PostStatusForm <PostStatusForm
class="panel-body" class="panel-body"
v-bind="params" v-bind="params"
:disable-polls="true"
:disable-visibility-selector="true"
:post-handler="doEditStatus"
@posted="closeModal" @posted="closeModal"
:disablePolls="true"
:disableVisibilitySelector="true"
:post-handler="doEditStatus"
/> />
</div> </div>
</Modal> </Modal>

View file

@ -249,8 +249,8 @@ const EmojiInput = {
* them, masto seem to be rendering :emoji::emoji: correctly now so why not * them, masto seem to be rendering :emoji::emoji: correctly now so why not
*/ */
const isSpaceRegex = /\s/ const isSpaceRegex = /\s/
const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : '' const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : ''
const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : '' const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : ''
const newValue = [ const newValue = [
before, before,

View file

@ -43,10 +43,7 @@
:class="{ highlighted: index === highlighted }" :class="{ highlighted: index === highlighted }"
@click.stop.prevent="onClick($event, suggestion)" @click.stop.prevent="onClick($event, suggestion)"
> >
<span <span v-if="!suggestion.mfm" class="image">
v-if="!suggestion.mfm"
class="image"
>
<img <img
v-if="suggestion.img" v-if="suggestion.img"
:src="suggestion.img" :src="suggestion.img"

View file

@ -122,14 +122,14 @@ export const suggestUsers = ({ dispatch, state }) => {
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1 const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
return diff + nameAlphabetically + screenNameAlphabetically return diff + nameAlphabetically + screenNameAlphabetically
/* eslint-disable camelcase */
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({ }).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
displayText: screen_name_ui, displayText: screen_name_ui,
detailText: name, detailText: name,
imageUrl: profile_image_url_original, imageUrl: profile_image_url_original,
replacement: '@' + screen_name + ' ' replacement: '@' + screen_name + ' '
})) }))
/* eslint-enable camelcase */
suggestions = newSuggestions || [] suggestions = newSuggestions || []
return suggestions return suggestions

View file

@ -7,7 +7,7 @@ import {
faStickyNote, faStickyNote,
faSmileBeam faSmileBeam
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { trim, escapeRegExp, startCase, debounce } from 'lodash' import { trim, escapeRegExp, startCase } from 'lodash'
library.add( library.add(
faBoxOpen, faBoxOpen,
@ -42,9 +42,6 @@ const EmojiPicker = {
EmojiGrid EmojiGrid
}, },
methods: { methods: {
debouncedSearch: debounce(function (e) {
this.keyword = e.target.value
}, 500),
onStickerUploaded (e) { onStickerUploaded (e) {
this.$emit('sticker-uploaded', e) this.$emit('sticker-uploaded', e)
}, },
@ -88,6 +85,17 @@ const EmojiPicker = {
activeGroupView () { activeGroupView () {
return this.showingStickers ? '' : this.activeGroup return this.showingStickers ? '' : this.activeGroup
}, },
stickersAvailable () {
if (this.$store.state.instance.stickers) {
return this.$store.state.instance.stickers.length > 0
}
return 0
},
filteredEmoji () {
return this.filterByKeyword(
this.$store.state.instance.customEmoji || []
)
},
emojis () { emojis () {
const recentEmojis = this.$store.getters.recentEmojis const recentEmojis = this.$store.getters.recentEmojis
const standardEmojis = this.$store.state.instance.emoji || [] const standardEmojis = this.$store.state.instance.emoji || []

View file

@ -44,10 +44,11 @@
> >
<div class="emoji-search"> <div class="emoji-search">
<input <input
v-model="keyword"
type="text" type="text"
class="form-control" class="form-control"
:placeholder="$t('emoji.search_emoji')" :placeholder="$t('emoji.search_emoji')"
@input="debouncedSearch" @input="$event.target.composing = false"
> >
</div> </div>
<EmojiGrid <EmojiGrid

View file

@ -1,6 +1,5 @@
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import UserListPopover from '../user_list_popover/user_list_popover.vue' import UserListPopover from '../user_list_popover/user_list_popover.vue'
import StillImage from '../still-image/still-image.vue'
const EMOJI_REACTION_COUNT_CUTOFF = 12 const EMOJI_REACTION_COUNT_CUTOFF = 12
@ -13,8 +12,7 @@ const EmojiReactions = {
name: 'EmojiReactions', name: 'EmojiReactions',
components: { components: {
UserAvatar, UserAvatar,
UserListPopover, UserListPopover
StillImage
}, },
props: ['status'], props: ['status'],
data: () => ({ data: () => ({

View file

@ -14,13 +14,12 @@
<span <span
v-if="reaction.url !== null" v-if="reaction.url !== null"
> >
<StillImage <img
:src="reaction.url" :src="reaction.url"
:title="reaction.name" :title="reaction.name"
:alt="reaction.name"
class="reaction-emoji" class="reaction-emoji"
height="2.55em" height="2.55em"
/> >
{{ reaction.count }} {{ reaction.count }}
</span> </span>
<span v-else> <span v-else>
@ -42,7 +41,7 @@
</div> </div>
</template> </template>
<script src="./emoji_reactions.js"></script> <script src="./emoji_reactions.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -136,26 +136,18 @@ const ExtraButtons = {
}, },
doRedraftStatus () { doRedraftStatus () {
this.$store.dispatch('fetchStatusSource', { id: this.status.id }) this.$store.dispatch('fetchStatusSource', { id: this.status.id })
.then(data => { .then(data => this.$store.dispatch('openPostStatusModal', {
let repliedUserId = this.status.in_reply_to_user_id; isRedraft: true,
let repliedUser = this.status.attentions.filter(user => statusId: this.status.id,
user.id === repliedUserId); subject: data.spoiler_text,
this.$store.dispatch('openPostStatusModal', { statusText: data.text,
isRedraft: true, statusIsSensitive: this.status.nsfw,
attentions: this.status.attentions, statusPoll: this.status.poll,
statusId: this.status.id, statusFiles: [...this.status.attachments],
subject: data.spoiler_text, statusScope: this.status.visibility,
statusText: data.text, statusLanguage: this.status.language,
statusIsSensitive: this.status.nsfw, statusContentType: data.content_type
statusPoll: this.status.poll, }))
statusFiles: [...this.status.attachments],
statusScope: this.status.visibility,
statusLanguage: this.status.language,
statusContentType: data.content_type,
replyTo: this.status.in_reply_to_status_id,
repliedUser: repliedUser
})
})
this.doDeleteStatus() this.doDeleteStatus()
}, },
showRedraftStatusConfirmDialog () { showRedraftStatusConfirmDialog () {

View file

@ -7,7 +7,7 @@
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
remove-padding remove-padding
> >
<template #content="{close}"> <template v-slot:content="{close}">
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <button
v-if="canMute && !status.thread_muted" v-if="canMute && !status.thread_muted"
@ -172,7 +172,7 @@
</button> </button>
</div> </div>
</template> </template>
<template #trigger> <template v-slot:trigger>
<button class="button-unstyled popover-trigger"> <button class="button-unstyled popover-trigger">
<FAIcon <FAIcon
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
@ -205,7 +205,7 @@
</Popover> </Popover>
</template> </template>
<script src="./extra_buttons.js"></script> <script src="./extra_buttons.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -35,7 +35,7 @@
</div> </div>
</template> </template>
<script src="./favorite_button.js"></script> <script src="./favorite_button.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
@ -55,9 +55,6 @@
.interactive { .interactive {
.svg-inline--fa { .svg-inline--fa {
@media (prefers-reduced-motion: reduce) {
animation: unset;
}
animation-duration: 0.6s; animation-duration: 0.6s;
} }

View file

@ -23,7 +23,7 @@
</div> </div>
</template> </template>
<script src="./features_panel.js"></script> <script src="./features_panel.js" ></script>
<style lang="scss"> <style lang="scss">
.features-panel li { .features-panel li {

View file

@ -1,53 +0,0 @@
import RuffleService from '../../services/ruffle_service/ruffle_service.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faStop,
faExclamationTriangle
} from '@fortawesome/free-solid-svg-icons'
library.add(
faStop,
faExclamationTriangle
)
const Flash = {
props: [ 'src' ],
data () {
return {
player: false, // can be true, "hidden", false. hidden = element exists
loaded: false,
ruffleInstance: null
}
},
methods: {
openPlayer () {
if (this.player) return // prevent double-loading, or re-loading on failure
this.player = 'hidden'
RuffleService.getRuffle().then((ruffle) => {
const player = ruffle.newest().createPlayer()
player.config = {
letterbox: 'on'
}
const container = this.$refs.container
container.appendChild(player)
player.style.width = '100%'
player.style.height = '100%'
player.load(this.src).then(() => {
this.player = true
}).catch((e) => {
console.error('Error loading ruffle', e)
this.player = 'error'
})
this.ruffleInstance = player
this.$emit('playerOpened')
})
},
closePlayer () {
this.ruffleInstance && this.ruffleInstance.remove()
this.player = false
this.$emit('playerClosed')
}
}
}
export default Flash

View file

@ -1,84 +0,0 @@
<template>
<div class="Flash">
<div
v-if="player === true || player === 'hidden'"
ref="container"
class="player"
:class="{ hidden: player === 'hidden' }"
/>
<button
v-if="player !== true"
class="button-unstyled placeholder"
@click="openPlayer"
>
<span
v-if="player === 'hidden'"
class="label"
>
{{ $t('general.loading') }}
</span>
<span
v-if="player === 'error'"
class="label"
>
{{ $t('general.flash_fail') }}
</span>
<span
v-else
class="label"
>
<p>
{{ $t('general.flash_content') }}
</p>
<p>
<FAIcon icon="exclamation-triangle" />
{{ $t('general.flash_security') }}
</p>
</span>
</button>
</div>
</template>
<script src="./flash.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.Flash {
display: inline-block;
width: 100%;
height: 100%;
position: relative;
.player {
height: 100%;
width: 100%;
}
.placeholder {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg);
color: var(--link);
}
.hider {
top: 0;
}
.label {
text-align: center;
flex: 1 1 0;
line-height: 1.2;
white-space: normal;
word-wrap: normal;
}
.hidden {
display: none;
visibility: 'hidden';
}
}
</style>

View file

@ -1,8 +1,5 @@
<template> <template>
<basic-user-card <basic-user-card :user="user" v-if="show">
v-if="show"
:user="user"
>
<div class="follow-request-card-content-container"> <div class="follow-request-card-content-container">
<button <button
class="btn button-default" class="btn button-default"

View file

@ -47,7 +47,7 @@
</div> </div>
</template> </template>
<script src="./font_control.js"></script> <script src="./font_control.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -31,8 +31,8 @@
:description="descriptions && descriptions[attachment.id]" :description="descriptions && descriptions[attachment.id]"
:hide-description="size === 'small' || tooManyAttachments && hidingLong" :hide-description="size === 'small' || tooManyAttachments && hidingLong"
:style="itemStyle(attachment.id, row.items)" :style="itemStyle(attachment.id, row.items)"
@set-media="onMedia" @setMedia="onMedia"
@natural-size-load="onNaturalSizeLoad" @naturalSizeLoad="onNaturalSizeLoad"
/> />
</div> </div>
</div> </div>

View file

@ -14,6 +14,6 @@
</span> </span>
</template> </template>
<script src="./hashtag_link.js" /> <script src="./hashtag_link.js"/>
<style lang="scss" src="./hashtag_link.scss" /> <style lang="scss" src="./hashtag_link.scss"/>

View file

@ -10,4 +10,4 @@
</div> </div>
</template> </template>
<script src="./instance_specific_panel.js"></script> <script src="./instance_specific_panel.js" ></script>

View file

@ -1,13 +1,9 @@
<template> <template>
<div <div class="list">
class="list"
role="list"
>
<div <div
v-for="item in items" v-for="item in items"
:key="getKey(item)" :key="getKey(item)"
class="list-item" class="list-item"
role="listitem"
> >
<slot <slot
name="item" name="item"
@ -42,7 +38,6 @@ export default {
@import '../../_variables.scss'; @import '../../_variables.scss';
.list { .list {
min-height: 1em;
&-item:not(:last-child) { &-item:not(:last-child) {
border-bottom: 1px solid; border-bottom: 1px solid;
border-bottom-color: $fallback--border; border-bottom-color: $fallback--border;

View file

@ -10,7 +10,7 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p>{{ $t("about.bubble_instances_description") }}:</p> <p>{{ $t("about.bubble_instances_description")}}:</p>
<ul> <ul>
<li <li
v-for="instance in bubbleInstances" v-for="instance in bubbleInstances"

View file

@ -90,7 +90,7 @@
</div> </div>
</template> </template>
<script src="./login_form.js"></script> <script src="./login_form.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -4,7 +4,6 @@ import Modal from '../modal/modal.vue'
import PinchZoom from '../pinch_zoom/pinch_zoom.vue' import PinchZoom from '../pinch_zoom/pinch_zoom.vue'
import SwipeClick from '../swipe_click/swipe_click.vue' import SwipeClick from '../swipe_click/swipe_click.vue'
import GestureService from '../../services/gesture_service/gesture_service' import GestureService from '../../services/gesture_service/gesture_service'
import Flash from 'src/components/flash/flash.vue'
import fileTypeService from '../../services/file_type/file_type.service.js' import fileTypeService from '../../services/file_type/file_type.service.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -27,8 +26,7 @@ const MediaModal = {
VideoAttachment, VideoAttachment,
PinchZoom, PinchZoom,
SwipeClick, SwipeClick,
Modal, Modal
Flash
}, },
data () { data () {
return { return {

View file

@ -2,7 +2,7 @@
<Modal <Modal
v-if="showing" v-if="showing"
class="media-modal-view" class="media-modal-view"
@backdrop-clicked="hideIfNotSwiped" @backdropClicked="hideIfNotSwiped"
> >
<SwipeClick <SwipeClick
v-if="type === 'image'" v-if="type === 'image'"
@ -48,13 +48,6 @@
:title="currentMedia.description" :title="currentMedia.description"
controls controls
/> />
<Flash
v-if="type === 'flash'"
class="modal-image"
:src="currentMedia.url"
:alt="currentMedia.description"
:title="currentMedia.description"
/>
<button <button
v-if="canNavigate" v-if="canNavigate"
:title="$t('media_modal.previous')" :title="$t('media_modal.previous')"

View file

@ -42,7 +42,7 @@ const mediaUpload = {
.then((fileData) => { .then((fileData) => {
self.$emit('uploaded', fileData) self.$emit('uploaded', fileData)
self.decreaseUploadCount() self.decreaseUploadCount()
}, (error) => { }, (error) => { // eslint-disable-line handle-callback-err
self.$emit('upload-failed', 'default') self.$emit('upload-failed', 'default')
self.decreaseUploadCount() self.decreaseUploadCount()
}) })

View file

@ -26,7 +26,7 @@
</label> </label>
</template> </template>
<script src="./media_upload.js"></script> <script src="./media_upload.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -93,6 +93,9 @@ const MentionLink = {
this.highlightType this.highlightType
] ]
}, },
useAtIcon () {
return this.mergedConfig.useAtIcon
},
isRemote () { isRemote () {
return this.userName !== this.userNameFull return this.userName !== this.userNameFull
}, },

View file

@ -66,6 +66,6 @@
</span> </span>
</template> </template>
<script src="./mention_link.js" /> <script src="./mention_link.js"/>
<style lang="scss" src="./mention_link.scss" /> <style lang="scss" src="./mention_link.scss"/>

View file

@ -37,5 +37,5 @@
</span> </span>
</span> </span>
</template> </template>
<script src="./mentions_line.js"></script> <script src="./mentions_line.js" ></script>
<style lang="scss" src="./mentions_line.scss" /> <style lang="scss" src="./mentions_line.scss" />

View file

@ -69,4 +69,4 @@
</div> </div>
</div> </div>
</template> </template>
<script src="./recovery_form.js"></script> <script src="./recovery_form.js" ></script>

View file

@ -18,7 +18,6 @@
<input <input
id="code" id="code"
v-model="code" v-model="code"
autocomplete="one-time-code"
class="form-control" class="form-control"
> >
</div> </div>

View file

@ -157,9 +157,6 @@
box-shadow: var(--panelShadow); box-shadow: var(--panelShadow);
transition-property: transform; transition-property: transform;
transition-duration: 0.25s; transition-duration: 0.25s;
@media (prefers-reduced-motion: reduce) {
transition: unset;
}
transform: translateX(0); transform: translateX(0);
z-index: 1001; z-index: 1001;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;

View file

@ -4,7 +4,7 @@
class="panel-heading" class="panel-heading"
@click="toggleHidden" @click="toggleHidden"
> >
<h4>{{ $t('moderation.reports.report') + ' ' + account.screen_name }}</h4> <h4>{{ $t('moderation.reports.report') + ' ' + this.account.screen_name }}</h4>
<button <button
v-if="isOpen" v-if="isOpen"
class="button-default" class="button-default"
@ -24,7 +24,7 @@
class="button-default" class="button-default"
@click.stop="updateReportState('open')" @click.stop="updateReportState('open')"
> >
{{ $t('moderation.reports.reopen') }} {{ $t('moderation.reports.reopen') }}
</button> </button>
</div> </div>
<div <div
@ -35,10 +35,7 @@
<div v-if="content"> <div v-if="content">
{{ decode(content) }} {{ decode(content) }}
</div> </div>
<i <i v-else class="faint">
v-else
class="faint"
>
{{ $t('moderation.reports.no_content') }} {{ $t('moderation.reports.no_content') }}
</i> </i>
<div class="report-author"> <div class="report-author">
@ -46,12 +43,12 @@
class="small-avatar" class="small-avatar"
:user="actor" :user="actor"
/> />
{{ actor.screen_name }} {{ this.actor.screen_name }}
</div> </div>
</div> </div>
<div <div
v-if="!hidden && statuses.length > 0"
class="dropdown" class="dropdown"
v-if="!hidden && this.statuses.length > 0"
> >
<button <button
class="button button-unstyled dropdown-header" class="button button-unstyled dropdown-header"
@ -77,8 +74,8 @@
</div> </div>
</div> </div>
<div <div
v-if="!hidden && notes.length > 0"
class="dropdown" class="dropdown"
v-if="!hidden && this.notes.length > 0"
> >
<button <button
class="button button-unstyled dropdown-header" class="button button-unstyled dropdown-header"
@ -102,9 +99,9 @@
</div> </div>
<div class="report-add-note"> <div class="report-add-note">
<textarea <textarea
v-model.trim="note"
rows="1" rows="1"
cols="1" cols="1"
v-model.trim="note"
:placeholder="$t('moderation.reports.note_placeholder')" :placeholder="$t('moderation.reports.note_placeholder')"
/> />
<button <button
@ -137,7 +134,7 @@
:offset="{ y: 5 }" :offset="{ y: 5 }"
remove-padding remove-padding
> >
<template #trigger> <template v-slot:trigger>
<button <button
class="btn button-default" class="btn button-default"
:disabled="!tagPolicyEnabled" :disabled="!tagPolicyEnabled"
@ -150,7 +147,7 @@
/> />
</button> </button>
</template> </template>
<template #content="{close}"> <template v-slot:content="{close}">
<div <div
class="dropdown-menu" class="dropdown-menu"
:disabled="!tagPolicyEnabled" :disabled="!tagPolicyEnabled"

View file

@ -6,7 +6,7 @@
class="small-avatar" class="small-avatar"
:user="user" :user="user"
/> />
{{ user.screen_name }} {{ this.user.screen_name }}
</div> </div>
<div class="header-right"> <div class="header-right">
<Timeago <Timeago

View file

@ -22,9 +22,6 @@ export default {
default: false default: false
} }
}, },
emits: [
'backdropClicked',
],
computed: { computed: {
classes () { classes () {
return { return {

View file

@ -8,7 +8,7 @@
@show="setToggled(true)" @show="setToggled(true)"
@close="setToggled(false)" @close="setToggled(false)"
> >
<template #content> <template v-slot:content>
<div class="dropdown-menu"> <div class="dropdown-menu">
<span v-if="user.is_local"> <span v-if="user.is_local">
<button <button
@ -122,7 +122,7 @@
</span> </span>
</div> </div>
</template> </template>
<template #trigger> <template v-slot:trigger>
<button <button
class="btn button-default btn-block moderation-tools-button" class="btn button-default btn-block moderation-tools-button"
:class="{ toggled }" :class="{ toggled }"
@ -137,11 +137,11 @@
v-if="showDeleteUserDialog" v-if="showDeleteUserDialog"
:on-cancel="deleteUserDialog.bind(this, false)" :on-cancel="deleteUserDialog.bind(this, false)"
> >
<template #header> <template v-slot:header>
{{ $t('user_card.admin_menu.delete_user') }} {{ $t('user_card.admin_menu.delete_user') }}
</template> </template>
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p> <p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
<template #footer> <template v-slot:footer>
<button <button
class="btn button-default" class="btn button-default"
@click="deleteUserDialog(false)" @click="deleteUserDialog(false)"

View file

@ -47,6 +47,9 @@ const NavPanel = {
} }
}, },
computed: { computed: {
compactNavPanel () {
return this.$store.getters.mergedConfig.compactNavPanel || false
},
...mapState({ ...mapState({
currentUser: state => state.users.currentUser, currentUser: state => state.users.currentUser,
privateMode: state => state.instance.private, privateMode: state => state.instance.private,

View file

@ -1,5 +1,8 @@
<template> <template>
<div class="NavPanel"> <div
class="NavPanel"
:class="{ compact: compactNavPanel }"
>
<div class="panel panel-default"> <div class="panel panel-default">
<ul> <ul>
<li v-if="currentUser || !privateMode"> <li v-if="currentUser || !privateMode">
@ -102,7 +105,7 @@
</div> </div>
</template> </template>
<script src="./nav_panel.js"></script> <script src="./nav_panel.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
@ -211,5 +214,83 @@
right: 0.6rem; right: 0.6rem;
top: 1.25em; top: 1.25em;
} }
&.compact {
.panel {
overflow: visible;
ul > li:hover > a:not(.router-link-active) > .button-icon {
color: var(--selectedMenuText,#b9b9ba);
}
ul > li > .router-link-active > .button-icon {
color: var(--selectedMenuLightText);
}
ul {
display: flex;
height: 100%;
padding: 0;
}
li {
width: -moz-available;
width: -webkit-fill-available;
border-bottom: none;
a {
border-radius: 0 !important;
}
}
.timelines-chevron {
display: none;
}
.timelines-background {
position: absolute;
z-index: 10000;
}
a, button {
font-size: 0;
height: 100%;
display: flex;
position: relative;
padding-top: 7px;
padding-bottom: 7px;
}
.button-icon, svg.svg-inline--fa {
margin: auto;
font-size: 20px;
color: var(--link,#d8a070);
}
.badge {
position: absolute;
right: 0;
background-color: red;
background-color: var(--badgeNotification,red);
color: #fff;
color: var(--badgeNotificationText,#fff);
// remove layout
padding: 0;
margin: 0;
box-shadow: black 0 1px 5px;
display: inline-block;
border-radius: 99px;
min-width: 22px;
line-height: 22px;
min-height: 22px;
max-height: 22px;
font-size: 14px;
font-weight: normal;
font-style: normal;
font-family: inherit;
}
}
}
} }
</style> </style>

View file

@ -1,12 +1,11 @@
import StatusContent from '../status_content/status_content.vue'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import Status from '../status/status.vue' import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue' import Timeago from '../timeago/timeago.vue'
import StatusContent from '../status_content/status_content.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import ConfirmModal from '../confirm_modal/confirm_modal.vue' import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import StillImage from '../still-image/still-image.vue'
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -51,8 +50,7 @@ const Notification = {
Timeago, Timeago,
Status, Status,
RichContent, RichContent,
ConfirmModal, ConfirmModal
StillImage
}, },
methods: { methods: {
toggleUserExpanded () { toggleUserExpanded () {

View file

@ -116,13 +116,12 @@
scope="global" scope="global"
keypath="notifications.reacted_with" keypath="notifications.reacted_with"
> >
<still-image <img
v-if="notification.emoji_url !== null" v-if="notification.emoji_url !== null"
class="notification-reaction-emoji" class="notification-reaction-emoji"
:src="notification.emoji_url" :src="notification.emoji_url"
:title="notification.emoji" :name="notification.emoji"
:alt="notification.emoji" >
/>
<span <span
v-else v-else
class="emoji-reaction-emoji" class="emoji-reaction-emoji"
@ -152,6 +151,7 @@
> >
<Timeago <Timeago
:time="notification.created_at" :time="notification.created_at"
:with-direction="true"
:auto-update="240" :auto-update="240"
/> />
</router-link> </router-link>

View file

@ -5,7 +5,7 @@
placement="bottom" placement="bottom"
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
> >
<template #content> <template v-slot:content>
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <button
class="button-default dropdown-item" class="button-default dropdown-item"
@ -72,7 +72,7 @@
</button> </button>
</div> </div>
</template> </template>
<template #trigger> <template v-slot:trigger>
<button class="filter-trigger-button button-unstyled"> <button class="filter-trigger-button button-unstyled">
<FAIcon icon="filter" /> <FAIcon icon="filter" />
</button> </button>

View file

@ -105,12 +105,9 @@
flex: 1; flex: 1;
padding-left: 0.8em; padding-left: 0.8em;
min-width: 0; min-width: 0;
}
.heading-right, .notification-right {
.timeago { .timeago {
display: inline-block; min-width: 3em;
min-width: 6em;
text-align: right; text-align: right;
} }
} }

View file

@ -14,7 +14,7 @@
:model-value="present" :model-value="present"
:disabled="disabled" :disabled="disabled"
class="opt" class="opt"
@update:model-value="$emit('update:modelValue', !present ? fallback : undefined)" @update:modelValue="$emit('update:modelValue', !present ? fallback : undefined)"
/> />
<input <input
:id="name" :id="name"

View file

@ -2,6 +2,7 @@
<pinch-zoom <pinch-zoom
class="pinch-zoom-parent" class="pinch-zoom-parent"
v-bind="$attrs" v-bind="$attrs"
v-on="$listeners"
> >
<slot /> <slot />
</pinch-zoom> </pinch-zoom>

View file

@ -114,7 +114,7 @@
svg { svg {
width: 22px; width: 22px;
margin-right: 0.75rem; margin-right: 0.75rem;
color: var(--popoverIcon, $fallback--icon) color: var(--menuPopoverIcon, $fallback--icon)
} }
} }

View file

@ -9,12 +9,11 @@ import StatusContent from '../status_content/status_content.vue'
import fileTypeService from '../../services/file_type/file_type.service.js' import fileTypeService from '../../services/file_type/file_type.service.js'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
import { reject, map, uniqBy, debounce } from 'lodash' import { reject, map, uniqBy, debounce } from 'lodash'
import { usePostLanguageOptions } from 'src/lib/post_language'
import suggestor from '../emoji_input/suggestor.js' import suggestor from '../emoji_input/suggestor.js'
import { mapGetters, mapState } from 'vuex' import { mapGetters, mapState } from 'vuex'
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import Select from '../select/select.vue' import Select from '../select/select.vue'
import iso6391 from 'iso-639-1'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -63,13 +62,6 @@ const deleteDraft = (draftKey) => {
localStorage.setItem('drafts', JSON.stringify(draftData)); localStorage.setItem('drafts', JSON.stringify(draftData));
} }
const interfaceToISOLanguage = (ilang) => {
const sep = ilang.indexOf("_");
return sep < 0 ?
ilang :
ilang.substr(0, sep);
}
const PostStatusForm = { const PostStatusForm = {
props: [ props: [
'statusId', 'statusId',
@ -137,13 +129,6 @@ const PostStatusForm = {
this.$refs.textarea.focus() this.$refs.textarea.focus()
} }
}, },
setup() {
const {postLanguageOptions} = usePostLanguageOptions()
return {
postLanguageOptions,
}
},
data () { data () {
const preset = this.$route.query.message const preset = this.$route.query.message
let statusText = preset || '' let statusText = preset || ''
@ -153,8 +138,7 @@ const PostStatusForm = {
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser) statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
} }
const { postContentType: contentType, postLanguage: defaultPostLanguage, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage, alwaysShowSubjectInput } = this.$store.getters.mergedConfig const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage } = this.$store.getters.mergedConfig
const postLanguage = defaultPostLanguage || interfaceToISOLanguage(interfaceLanguage)
let statusParams = { let statusParams = {
spoilerText: this.subject || '', spoilerText: this.subject || '',
@ -165,7 +149,7 @@ const PostStatusForm = {
poll: {}, poll: {},
mediaDescriptions: {}, mediaDescriptions: {},
visibility: this.suggestedVisibility(), visibility: this.suggestedVisibility(),
language: postLanguage, language: interfaceLanguage,
contentType contentType
} }
@ -180,7 +164,7 @@ const PostStatusForm = {
poll: this.statusPoll || {}, poll: this.statusPoll || {},
mediaDescriptions: this.statusMediaDescriptions || {}, mediaDescriptions: this.statusMediaDescriptions || {},
visibility: this.statusScope || this.suggestedVisibility(), visibility: this.statusScope || this.suggestedVisibility(),
language: this.statusLanguage || postLanguage, language: this.statusLanguage || interfaceLanguage,
contentType: statusContentType contentType: statusContentType
} }
} }
@ -215,10 +199,6 @@ const PostStatusForm = {
} }
} }
// When first loading the form, hide the subject (CW) field if it's disabled or doesn't have a starting value.
// "disableSubject" seems to take priority over "alwaysShowSubjectInput".
const showSubject = !this.disableSubject && (statusParams.spoilerText || alwaysShowSubjectInput)
return { return {
dropFiles: [], dropFiles: [],
uploadingFiles: false, uploadingFiles: false,
@ -233,10 +213,7 @@ const PostStatusForm = {
preview: null, preview: null,
previewLoading: false, previewLoading: false,
emojiInputShown: false, emojiInputShown: false,
idempotencyKey: '', idempotencyKey: ''
activeEmojiInput: undefined,
activeTextInput: undefined,
subjectVisible: showSubject
} }
}, },
computed: { computed: {
@ -325,6 +302,9 @@ const PostStatusForm = {
...mapState({ ...mapState({
mobileLayout: state => state.interface.mobileLayout mobileLayout: state => state.interface.mobileLayout
}), }),
isoLanguages () {
return iso6391.getAllCodes();
}
}, },
watch: { watch: {
'newStatus': { 'newStatus': {
@ -509,6 +489,7 @@ const PostStatusForm = {
} }
}, },
addMediaFile (fileInfo) { addMediaFile (fileInfo) {
this.$emit('resize')
this.newStatus.files.push(fileInfo) this.newStatus.files.push(fileInfo)
if (this.$store.getters.mergedConfig.sensitiveIfSubject && this.newStatus.spoilerText !== '') { if (this.$store.getters.mergedConfig.sensitiveIfSubject && this.newStatus.spoilerText !== '') {
@ -517,6 +498,7 @@ const PostStatusForm = {
this.$emit('resize', { delayed: true }) this.$emit('resize', { delayed: true })
}, },
removeMediaFile (fileInfo) { removeMediaFile (fileInfo) {
this.$emit('resize')
let index = this.newStatus.files.indexOf(fileInfo) let index = this.newStatus.files.indexOf(fileInfo)
this.newStatus.files.splice(index, 1) this.newStatus.files.splice(index, 1)
this.$emit('resize') this.$emit('resize')
@ -694,33 +676,8 @@ const PostStatusForm = {
this.$refs['emoji-input'].resize() this.$refs['emoji-input'].resize()
}, },
showEmojiPicker () { showEmojiPicker () {
if (!this.activeEmojiInput || !this.activeTextInput) this.$refs['textarea'].focus()
this.focusStatusInput() this.$refs['emoji-input'].triggerShowPicker()
this.$refs[this.activeTextInput].focus()
this.$refs[this.activeEmojiInput].triggerShowPicker()
},
focusStatusInput() {
this.activeEmojiInput = 'emoji-input'
this.activeTextInput = 'textarea'
},
focusSubjectInput() {
this.activeEmojiInput = 'subject-emoji-input'
this.activeTextInput = 'subject-input'
},
toggleSubjectVisible() {
// If hiding CW, then we need to clear the subject and reset focus
if (this.subjectVisible)
{
this.focusStatusInput()
// "nsfw" property is normally set by the @change listener, but this bypasses it.
// We need to clear it manually instead.
this.newStatus.spoilerText = ''
this.newStatus.nsfw = false
}
this.subjectVisible = !this.subjectVisible
}, },
clearError () { clearError () {
this.error = null this.error = null

View file

@ -118,16 +118,13 @@
/> />
</div> </div>
<EmojiInput <EmojiInput
v-if="subjectVisible" v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)"
ref="subject-emoji-input"
v-model="newStatus.spoilerText" v-model="newStatus.spoilerText"
enable-emoji-picker enable-emoji-picker
hide-emoji-button
:suggest="emojiSuggestor" :suggest="emojiSuggestor"
class="form-control" class="form-control"
> >
<input <input
ref="subject-input"
v-model="newStatus.spoilerText" v-model="newStatus.spoilerText"
type="text" type="text"
:placeholder="$t('post_status.content_warning')" :placeholder="$t('post_status.content_warning')"
@ -135,7 +132,6 @@
size="1" size="1"
class="form-post-subject" class="form-post-subject"
@input="onSubjectInput" @input="onSubjectInput"
@focus="focusSubjectInput()"
> >
</EmojiInput> </EmojiInput>
<i18n-t <i18n-t
@ -170,14 +166,13 @@
cols="1" cols="1"
:disabled="posting && !optimisticPosting" :disabled="posting && !optimisticPosting"
class="form-post-body" class="form-post-body"
:class="{ 'scrollable-form': !!maxHeight, '-has-subject': subjectVisible }" :class="{ 'scrollable-form': !!maxHeight }"
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)" @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
@keydown.meta.enter="postStatus($event, newStatus)" @keydown.meta.enter="postStatus($event, newStatus)"
@keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)" @keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
@input="resize" @input="resize"
@compositionupdate="resize" @compositionupdate="resize"
@paste="paste" @paste="paste"
@focus="focusStatusInput()"
/> />
<p <p
v-if="hasStatusLengthLimit" v-if="hasStatusLengthLimit"
@ -190,7 +185,6 @@
<div <div
v-if="!disableScopeSelector" v-if="!disableScopeSelector"
class="visibility-tray" class="visibility-tray"
:class="{ 'visibility-tray-edit': isEdit }"
> >
<scope-selector <scope-selector
v-if="!disableVisibilitySelector" v-if="!disableVisibilitySelector"
@ -201,50 +195,47 @@
/> />
<div <div
class="format-selector-container"> class="language-selector"
<div
class="format-selector"
>
<Select
id="post-language"
v-model="newStatus.language"
class="form-control"
>
<option
v-for="language in postLanguageOptions"
:key="language.key"
:value="language.value"
>
{{ language.label }}
</option>
</Select>
</div>
<div
v-if="postFormats.length > 1"
class="text-format format-selector"
> >
<Select <Select
id="post-content-type" id="post-language"
v-model="newStatus.contentType" v-model="newStatus.language"
class="form-control" class="form-control"
>
<option
v-for="postFormat in postFormats"
:key="postFormat"
:value="postFormat"
>
{{ $t(`post_status.content_type["${postFormat}"]`) }}
</option>
</Select>
</div>
<div
v-if="postFormats.length === 1 && postFormats[0] !== 'text/plain'"
class="text-format format-selector"
> >
<span class="only-format"> <option
{{ $t(`post_status.content_type["${postFormats[0]}"]`) }} v-for="language in isoLanguages"
</span> :key="language"
</div> :value="language"
>
{{ language }}
</option>
</Select>
</div>
<div
v-if="postFormats.length > 1"
class="text-format"
>
<Select
id="post-content-type"
v-model="newStatus.contentType"
class="form-control"
>
<option
v-for="postFormat in postFormats"
:key="postFormat"
:value="postFormat"
>
{{ $t(`post_status.content_type["${postFormat}"]`) }}
</option>
</Select>
</div>
<div
v-if="postFormats.length === 1 && postFormats[0] !== 'text/plain'"
class="text-format"
>
<span class="only-format">
{{ $t(`post_status.content_type["${postFormats[0]}"]`) }}
</span>
</div> </div>
</div> </div>
</div> </div>
@ -285,15 +276,6 @@
> >
<FAIcon icon="poll-h" /> <FAIcon icon="poll-h" />
</button> </button>
<button
v-if="!disableSubject"
class="spoiler-icon button-unstyled"
:class="{ selected: subjectVisible }"
:title="$t('post_status.toggle_content_warning')"
@click="toggleSubjectVisible"
>
<FAIcon icon="eye-slash" />
</button>
</div> </div>
<button <button
v-if="posting" v-if="posting"
@ -309,14 +291,12 @@
> >
{{ $t('post_status.post') }} {{ $t('post_status.post') }}
</button> </button>
<!-- To keep the OSK at the same position after a message send, --> <!-- touchstart is used to keep the OSK at the same position after a message send -->
<!-- @touchstart.stop.prevent was used. But while OSK position is -->
<!-- quirky, accidental mobile posts caused by the workaround -->
<!-- when people tried to scroll were a more serious bug. -->
<button <button
v-else v-else
:disabled="uploadingFiles || disableSubmit" :disabled="uploadingFiles || disableSubmit"
class="btn button-default" class="btn button-default"
@touchstart.stop.prevent="postStatus($event, newStatus)"
@click.stop.prevent="postStatus($event, newStatus)" @click.stop.prevent="postStatus($event, newStatus)"
> >
{{ $t('post_status.post') }} {{ $t('post_status.post') }}
@ -464,10 +444,6 @@
align-items: baseline; align-items: baseline;
} }
.visibility-tray-edit {
justify-content: right;
}
.visibility-notice.edit-warning { .visibility-notice.edit-warning {
> :first-child { > :first-child {
margin-top: 0; margin-top: 0;
@ -478,13 +454,7 @@
} }
} }
.format-selector-container { .media-upload-icon, .poll-icon, .emoji-icon {
.format-selector {
display: inline-block;
}
}
.media-upload-icon, .poll-icon, .emoji-icon, .spoiler-icon {
font-size: 1.85em; font-size: 1.85em;
line-height: 1.1; line-height: 1.1;
flex: 1; flex: 1;
@ -527,11 +497,6 @@
.poll-icon { .poll-icon {
order: 3; order: 3;
justify-content: center;
}
.spoiler-icon {
order: 4;
justify-content: right; justify-content: right;
} }
@ -584,11 +549,6 @@
line-height: 1.85; line-height: 1.85;
} }
.form-post-subject {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.form-post-body { .form-post-body {
// TODO: make a resizable textarea component? // TODO: make a resizable textarea component?
box-sizing: content-box; // needed for easier computation of dynamic size box-sizing: content-box; // needed for easier computation of dynamic size
@ -601,11 +561,6 @@
min-height: calc(var(--post-line-height) * 1em); min-height: calc(var(--post-line-height) * 1em);
resize: none; resize: none;
&.-has-subject {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
&.scrollable-form { &.scrollable-form {
overflow-y: auto; overflow-y: auto;
} }
@ -661,4 +616,5 @@
border: 2px dashed var(--text, $fallback--text); border: 2px dashed var(--text, $fallback--text);
} }
} }
</style> </style>

View file

@ -3,7 +3,7 @@
v-if="isLoggedIn && !resettingForm" v-if="isLoggedIn && !resettingForm"
:is-open="modalActivated" :is-open="modalActivated"
class="post-form-modal-view" class="post-form-modal-view"
@backdrop-clicked="closeModal" @backdropClicked="closeModal"
> >
<div class="post-form-modal-panel panel"> <div class="post-form-modal-panel panel">
<div class="panel-heading"> <div class="panel-heading">

View file

@ -8,13 +8,13 @@
remove-padding remove-padding
@show="focusInput" @show="focusInput"
> >
<template #content="{close}"> <template v-slot:content="{close}">
<EmojiPicker <EmojiPicker
:enable-sticker-picker="false" :enable-sticker-picker="false"
@emoji="addReaction($event, close)" @emoji="addReaction($event, close)"
/> />
</template> </template>
<template #trigger> <template v-slot:trigger>
<button <button
class="button-unstyled popover-trigger" class="button-unstyled popover-trigger"
:title="$t('tool_tip.add_reaction')" :title="$t('tool_tip.add_reaction')"
@ -28,7 +28,7 @@
</Popover> </Popover>
</template> </template>
<script src="./react_button.js"></script> <script src="./react_button.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -2,7 +2,7 @@ export default {
props: [ 'user' ], props: [ 'user' ],
computed: { computed: {
subscribeUrl () { subscribeUrl () {
// eslint-disable-next-line no-undef
const serverUrl = new URL(this.user.statusnet_profile_url) const serverUrl = new URL(this.user.statusnet_profile_url)
return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus` return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus`
} }

View file

@ -54,7 +54,7 @@
</div> </div>
</template> </template>
<script src="./retweet_button.js"></script> <script src="./retweet_button.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
@ -74,9 +74,6 @@
.interactive { .interactive {
.svg-inline--fa { .svg-inline--fa {
@media (prefers-reduced-motion: reduce) {
animation: unset;
}
animation-duration: 0.6s; animation-duration: 0.6s;
} }

Some files were not shown because too many files have changed in this diff Show more