Compare commits

...

236 commits

Author SHA1 Message Date
c83291ecff Merge pull request 'Sync develop with 2023.02 stable branch' (#6) from stable into develop
Reviewed-on: fedward/pleroma-fe#6
2023-02-12 19:20:33 +00:00
0ba3fa2311 Merge pull request 'Catch up to 2023.02 stable release' (#5) from AkkomaGang/akkoma-fe:stable into stable
Reviewed-on: fedward/pleroma-fe#5
2023-02-12 19:15:20 +00:00
8569b5946e Merge branch 'develop' into stable 2023-02-11 10:50:04 +00:00
6a9d169e24 Merge pull request 'components: emoji_reactions: force custom emoji reaction height' (#280) from a1batross/akkoma-fe:a1batross-patch-1 into develop
Reviewed-on: AkkomaGang/akkoma-fe#280
2023-02-11 10:41:00 +00:00
581c53a15e components: emoji_reactions: force custom emoji reaction height
Prevents the usage of too long emoji reactions
2023-02-10 23:28:46 +00:00
9e04e4fd80 Improve emoji picker performance (#275)
A simple virtual scroller is now used for the emoji grid. This avoids loading all emoji images at once, saving network bandwidth and reducing load on the server, while also putting less work on the browser's DOM and layout engine.

Co-authored-by: yan <yan@omg.lol>
Reviewed-on: AkkomaGang/akkoma-fe#275
Co-authored-by: yanchan09 <yan@omg.lol>
Co-committed-by: yanchan09 <yan@omg.lol>
2023-02-04 21:10:06 +00:00
88d5149db5 paginate-follow-requests (#277)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/akkoma-fe#277
2023-02-04 21:09:09 +00:00
b4b13d777f Merge pull request 'Add indicator to user card if user blocks you' (#274) from eris/pleroma-fe:block-indicator into develop
Reviewed-on: AkkomaGang/akkoma-fe#274
2023-01-27 10:08:17 +00:00
7f4dd9ff03 Disable follow button if blocked by user 2023-01-27 00:30:47 +00:00
a9a95e9120 Add indicator if user blocks you 2023-01-27 00:30:30 +00:00
56fd2e773b Merge branch 'languages' into develop 2023-01-15 17:59:50 +00:00
42dc1a027a add language input 2023-01-15 17:59:32 +00:00
236bc2c762 Merge pull request 'Only show "keep open" emoji checkbox on post form' (#269) from sfr/pleroma-fe:fix/keepopen into develop
Reviewed-on: AkkomaGang/akkoma-fe#269
2023-01-09 22:20:31 +00:00
Sol Fisher Romanoff
e9f47509ae
Only show "keep open" emoji checkbox on post form 2023-01-03 16:04:26 +02:00
f288d0c219 Make everything work with a strict CSP 2023-01-02 15:16:42 +00:00
d973396c96 Remove console.log 2023-01-01 21:06:02 +00:00
62287fffae add follow/unfollow to followed tags list 2023-01-01 21:05:25 +00:00
e9f16af82d Add list of followed hashtags to profile 2023-01-01 20:11:07 +00:00
dfba8be134 Fall back to nsfw image if no blurhash 2022-12-30 05:03:25 +00:00
313ddcebcb Add blurhash support 2022-12-30 04:57:23 +00:00
236b19e854 Merge branch 'develop' of akkoma.dev:AkkomaGang/akkoma-fe into develop 2022-12-30 03:20:30 +00:00
ea941d7cfa remove IHBA assets 2022-12-30 03:20:12 +00:00
2e5001e5de Allow follow(er) lists to be acessible by account owner even if follower counts are disabled (#246)
Currently, if a user has their follower/follow counts hidden, they cannot access their own list of followers/follows. This makes no real sense and means that they cannot modify those lists without disabling their privacy options.

This fix simply allows those tabs to be accessed no matter if the counts are hidden or not.

Reviewed-on: AkkomaGang/akkoma-fe#246
Co-authored-by: Beefox <bee@beefox.xyz>
Co-committed-by: Beefox <bee@beefox.xyz>
2022-12-30 03:04:15 +00:00
014f8b0dd2 Make minimum width for 3-column layout 1280px (#255) (#256)
1280px is a pretty common screen width for several resolutions
(1280x720, 1280x800, 1280x1024, etc.). Since it is only 20px less than
the current 1300px minimum, this shouldn't be a big issue to lower the
minimum screen width for the 3-column layout to 1280px.

Closes: AkkomaGang/pleroma-fe#255

Co-authored-by: Francis Dinh <normandy@biribiri.dev>
Reviewed-on: AkkomaGang/akkoma-fe#256
Co-authored-by: Norm <normandy@biribiri.dev>
Co-committed-by: Norm <normandy@biribiri.dev>
2022-12-30 03:01:17 +00:00
dd403b295f Merge pull request 'Remove stray debug log' (#265) from sfr/pleroma-fe:del-log into develop
Reviewed-on: AkkomaGang/akkoma-fe#265
2022-12-30 03:00:49 +00:00
Sol Fisher Romanoff
9cd62fe08d
Remove stray debug log 2022-12-30 01:03:31 +02:00
f668455dff Merge branch 'link-verification' into develop 2022-12-29 20:56:22 +00:00
5a4315384e force CI build 2022-12-29 15:25:03 +00:00
401dfa8fa6 update readme 2022-12-29 15:22:06 +00:00
bb243168b3 Revert "Merge pull request 'Don't show timeline links if disabled and logged out' (#250) from sfr/pleroma-fe:fix/hide-timelines into develop"
This reverts commit 0b5793c1e0, reversing
changes made to 72ef2e7454.
2022-12-29 15:18:13 +00:00
da491f3278 add verification of links 2022-12-29 15:17:35 +00:00
d00e28d5e9 fix emoji picker in replies in notifications 2022-12-22 05:43:01 +00:00
7ff17ab722 don't crash out if notification status is null 2022-12-20 13:20:13 +00:00
b009428814 Merge pull request 'Revert "Revert "use v1 urls""' (#254) from v1-urls into develop
Reviewed-on: AkkomaGang/pleroma-fe#254
2022-12-14 12:09:03 +00:00
7bec96a1bf Merge pull request 'Fix user moderation dropdown clipping' (#249) from sfr/pleroma-fe:fix/dropdown into develop
Reviewed-on: AkkomaGang/pleroma-fe#249
2022-12-14 12:08:27 +00:00
0b5793c1e0 Merge pull request 'Don't show timeline links if disabled and logged out' (#250) from sfr/pleroma-fe:fix/hide-timelines into develop
Reviewed-on: AkkomaGang/pleroma-fe#250
2022-12-14 12:08:08 +00:00
72ef2e7454 Merge pull request 'Fix 404 when reacting with Keycap Number Sign' (#252) from fef/pleroma-fe:develop into develop
Reviewed-on: AkkomaGang/pleroma-fe#252
2022-12-14 12:07:27 +00:00
c39332c1bf Revert "Revert "use v1 urls""
This reverts commit 8c6cf86de3.
2022-12-14 09:39:01 +00:00
8c6cf86de3 Revert "use v1 urls"
This reverts commit 909271c764.
2022-12-14 09:38:46 +00:00
909271c764 use v1 urls 2022-12-14 09:38:07 +00:00
fef
413acbc7dd
fix 404 when reacting with Keycap Number Sign
The Unicode sequence for the Keycap Number Sign
emoji starts with an ASCII "#" character, which
the browser's URL parser will interpret as a URI
fragment and truncate it before sending the
request to the backend.
2022-12-12 18:59:57 +01:00
Sol Fisher Romanoff
6e1ba218df
Don't show timeline links if disabled and logged out 2022-12-10 21:39:50 +02:00
Sol Fisher Romanoff
830e8fdb45
Fix user moderation dropdown clipping 2022-12-10 21:03:12 +02:00
9c9b4cc07c Merge branch 'develop' into stable 2022-12-10 14:52:00 +00:00
9bf310d509 bump version 2022-12-10 14:51:08 +00:00
e3e8b19df3 fix ES translation having weird o in a key 2022-12-10 00:17:33 +00:00
Weblate
e86c7abb39 Merge branch 'origin/develop' into Weblate. 2022-12-08 18:41:37 +00:00
8a0da8861d Merge pull request 'Add YAML bug and feat templates' (#247) from sfr/pleroma-fe:issue-template into develop
Reviewed-on: AkkomaGang/pleroma-fe#247
2022-12-08 18:41:36 +00:00
Sol Fisher Romanoff
6c7e691aea
Add YAML bug and feat templates 2022-12-08 20:24:21 +02:00
Weblate
b33d15a739 Merge branch 'origin/develop' into Weblate. 2022-12-07 22:37:54 +00:00
40e86998e6 Update 'ISSUE_TEMPLATE.md' 2022-12-07 22:37:52 +00:00
Weblate
177f344033 Merge branch 'origin/develop' into Weblate. 2022-12-07 22:32:07 +00:00
9079ac4afa Update 'ISSUE_TEMPLATE.md' 2022-12-07 22:31:49 +00:00
Weblate
dfc4e0a026 Translated using Weblate (Japanese (ja_PEDANTIC))
Currently translated at 99.8% (1031 of 1033 strings)

Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ja_PEDANTIC/
Translation: Pleroma fe/pleroma-fe
2022-12-07 22:31:39 +00:00
Weblate
3d732d1d28 Translated using Weblate (Indonesian)
Currently translated at 59.4% (614 of 1033 strings)

Translated using Weblate (Indonesian)

Currently translated at 53.0% (548 of 1033 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: t1 <taaa@fedora.email>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/id/
Translation: Pleroma fe/pleroma-fe
2022-12-07 22:31:39 +00:00
Weblate
e8ee31afed Translated using Weblate (English)
Currently translated at 100.0% (1033 of 1033 strings)

Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/en/
Translation: Pleroma fe/pleroma-fe
2022-12-07 22:31:39 +00:00
Weblate
d9d6b1e80b Translated using Weblate (Spanish)
Currently translated at 89.2% (918 of 1029 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: taretka <info@tarteka.net>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/es/
Translation: Pleroma fe/pleroma-fe
2022-12-07 22:31:39 +00:00
Weblate
1dd7a89544 Translated using Weblate (German)
Currently translated at 93.9% (967 of 1029 strings)

Co-authored-by: Johann <johann@qwertqwefsday.eu>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/de/
Translation: Pleroma fe/pleroma-fe
2022-12-07 22:31:38 +00:00
d3280c4ab3 Add issue template 2022-12-07 22:31:29 +00:00
abc75c360b Ensure only content gets clipped 2022-12-07 11:01:58 +00:00
a8e119b0f1 Merge branch 'develop' of akkoma.dev:AkkomaGang/pleroma-fe into develop 2022-12-06 15:56:06 +00:00
17e574b173 Move theme apply/reset to new row
Fixes #225
2022-12-06 15:55:39 +00:00
71d2e0b0ce Merge pull request 'fix scope selector icon spacing' (#243) from nocebo/crt-fe:shared/fix-scope-spacing into develop
Reviewed-on: AkkomaGang/pleroma-fe#243
2022-12-06 15:32:40 +00:00
b68e968bf9 Add ability to include custom CSS 2022-12-06 15:26:16 +00:00
eb49295422 Add hashtag following button (#244)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/pleroma-fe#244
2022-12-04 17:31:41 +00:00
337a30fe01 remove whitespace between scope selector icons
when i originally wrote this, for reasons unclear to the present me, i used literal whitespaces to space out the icons on the scope selector 
this causes strange inconsistencies in spacing depending on the font being used 
akkoma also did not include the whitespace when adding the local-only scope, resulting in even weirder spacing 
this corrects all of that by removing the whitespaces and using css instead
2022-12-03 07:12:55 +00:00
105ecd3836 Merge pull request 'pwa config' (#242) from pwa into develop
Reviewed-on: AkkomaGang/pleroma-fe#242
2022-12-02 12:01:43 +00:00
a3e490edcd use cutout of 512 logo 2022-12-02 12:00:30 +00:00
f8f5e1c89b fix SW path 2022-12-02 11:57:45 +00:00
e132814478 Register serviceworker 2022-12-02 11:56:15 +00:00
6af1df8bef Add logo files 2022-12-02 11:27:24 +00:00
b86f12cede Merge pull request 'Add a small margin to search bar' (#240) from karl/pleroma-fe:search-bar-margin into develop
Reviewed-on: AkkomaGang/pleroma-fe#240
2022-12-02 10:21:32 +00:00
Karl Prieb
c669701762 add a left margin on search bar 2022-11-29 18:04:33 -03:00
0900a9d87b Merge pull request 'Add post expiry inputs' (#239) from default-post-expiry into develop
Reviewed-on: AkkomaGang/pleroma-fe#239
2022-11-28 13:35:08 +00:00
0a01a2bdf0 Add post expiry inputs 2022-11-28 12:08:18 +00:00
7860c885c4 Add link to RSS feed to the profile (#234)
Today I learned that akkoma and mastodon (and potentially other activitypub services) offer RSS/Atom feeds for user profiles at `[user profile url].rss`. This PR adds a direct link to the feed because I haven’t seen anything link to the feed on either mastodon-fe or pleroma-fe

Co-authored-by: Charlotte 🦝 Delenk <lotte@chir.rs>
Reviewed-on: AkkomaGang/pleroma-fe#234
Co-authored-by: darkkirb <lotte@chir.rs>
Co-committed-by: darkkirb <lotte@chir.rs>
2022-11-26 20:57:04 +00:00
1c3bd60af2 Merge pull request 'fix formatting for large number of favorites/interactions' (#236) from drudge/pleroma-fe:develop into develop
Reviewed-on: AkkomaGang/pleroma-fe#236
2022-11-26 18:32:35 +00:00
Sean Meininger
b8faee5d6d added maintainer code 2022-11-26 00:15:12 -08:00
c01c62f149 Redirect users to "awaiting approval" message on register (#231)
Ref #81 - this implements a "your request has been sent" message

 ![image](/attachments/61dc3f5e-2706-46f9-a1ca-4efe3f526ff3)

Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/pleroma-fe#231
2022-11-22 14:44:44 +00:00
105b934f90 Only reload user if it _is_ a user (#232)
Ref #181

Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/pleroma-fe#232
2022-11-22 14:40:25 +00:00
b1f41add0e Don't error out if we can't fetch reports 2022-11-21 17:08:53 +00:00
e4e8ed812b Merge pull request 'Allow using mouse wheel to navigate through the emoji tabs (#179)' (#222) from Mergan/pleroma-fe:emoji-picker-allow-scroll into develop
Reviewed-on: AkkomaGang/pleroma-fe#222
2022-11-21 16:51:30 +00:00
684894aee3 mobile-newline (#226)
Allows the handle of users to drop down onto the next line if there isn't enough room in order to improve useability on mobile

Reviewed-on: AkkomaGang/pleroma-fe#226
Co-authored-by: Beefox <bee@beefox.xyz>
Co-committed-by: Beefox <bee@beefox.xyz>
2022-11-21 16:49:18 +00:00
f8a796b234 Merge pull request 'move domain block to drop down menu (#223)' (#224) from nocebo/crt-fe:shared/move-domain-mute into develop
Reviewed-on: AkkomaGang/pleroma-fe#224
2022-11-21 16:46:29 +00:00
70ea9e772c Merge pull request 'Allow for searching unicode emoji via inputting emoji (#163 & #227)' (#230) from Mergan/pleroma-fe:beefox-emoji-search into develop
Reviewed-on: AkkomaGang/pleroma-fe#230
2022-11-21 10:11:19 +00:00
efe0f53736 Constrain content to status content (#218) (#220)
MFM No longer overflows
![image](/attachments/7bbf519a-9fd2-492d-aba0-9a0e1ded6373)

Co-authored-by: David <dmgf2008@hotmail.com>
Reviewed-on: AkkomaGang/pleroma-fe#220
Co-authored-by: Mergan <mergan@noreply.akkoma>
Co-committed-by: Mergan <mergan@noreply.akkoma>
2022-11-21 10:10:50 +00:00
fcbbbad8d4 Allow for searching unicode emoji via inputting emoji
(This is needed for the react menu)
2022-11-21 01:04:00 -08:00
39b6b0b49f Merge remote-tracking branch 'origin/translations' into develop 2022-11-20 22:26:32 +00:00
867a86d887 Fix build 2022-11-20 22:26:16 +00:00
7538369fa1 Copy conversation display style from instance
Fixes #210
2022-11-20 22:18:34 +00:00
Weblate
2d4b2f2e20 Translated using Weblate (Spanish)
Currently translated at 87.1% (893 of 1025 strings)

Co-authored-by: taretka <info@tarteka.net>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/es/
Translation: Pleroma fe/pleroma-fe
2022-11-19 21:22:45 +00:00
862c93706c move domain block to drop down menu 2022-11-18 08:49:57 -05:00
David
e06348ee33 Allow using mouse wheel to navigate through the emoji tabs 2022-11-17 14:45:32 -08:00
6eafeed846 Merge pull request 'Merge changes from unauth-global-timeline branch' (#4) from develop into stable
Reviewed-on: fedward/pleroma-fe#4
2022-11-16 19:11:56 +00:00
5e514e8f0a Merge pull request 'unauth-global-timeline' (#3) from unauth-global-timeline into develop
Reviewed-on: fedward/pleroma-fe#3
2022-11-16 18:40:51 +00:00
ce6d06f998 revert 328fef9163
This work went into a feature branch. Revert dev before applying the changes from that branch.
2022-11-16 18:38:13 +00:00
cde3e55634 Fix style of new setter and refer to new value in desktop_nav 2022-11-16 13:28:58 -05:00
021216d5e3 Missed a comma. Oops. 2022-11-16 13:19:12 -05:00
c7d8cabe9c Testing method for setting when to display global timeline. 2022-11-16 13:07:57 -05:00
9c623de28c Merge pull request 'Hide global timeline for unauthorized users' (#2) from develop into stable
Reviewed-on: fedward/pleroma-fe#2
2022-11-15 18:14:25 +00:00
328fef9163 Hide global timeline for unauthorized users
The better way to make this change would be to have it set automatically in association with the Restrict Unauthenticated setting for the federated timeline, but for the moment this hack will do.
2022-11-15 18:13:45 +00:00
1ae90fb953 Merge pull request 'Change UI defaults for local instance' (#1) from develop into stable
Reviewed-on: fedward/pleroma-fe#1
2022-11-15 17:24:54 +00:00
27f3549f26 Update default text for new posts 2022-11-15 17:21:30 +00:00
d1a1e10c8b Always show instance panel on about page 2022-11-15 17:16:57 +00:00
2c9b73646c Merge pull request 'hotfix: mfm mysteries' (#215) from develop into stable
Reviewed-on: AkkomaGang/pleroma-fe#215
2022-11-15 16:01:07 +00:00
169282ea42 rely on backend mfm parsing 2022-11-15 15:54:16 +00:00
db46879a8f Big 'ol set of patches and dep maintenance (#212)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/pleroma-fe#212
2022-11-15 15:47:32 +00:00
80a519d7e4 Merge pull request 'hotfix: translation' (#207) from develop into stable
Reviewed-on: AkkomaGang/pleroma-fe#207
2022-11-12 19:08:20 +00:00
0770981a20 Ensure we don't fail if translation is disabled 2022-11-12 19:06:39 +00:00
c2a5a8c91f Merge pull request 'Use correct pluralization in reports tab' (#206) from sfr/pleroma-fe:fix/reports-plural into develop
Reviewed-on: AkkomaGang/pleroma-fe#206
2022-11-12 16:38:26 +00:00
Sol Fisher Romanoff
a1c0642bb5
Use correct pluralization in reports tab 2022-11-12 18:35:25 +02:00
975f04bf5a Merge pull request '2022.11 stable release' (#202) from develop into stable
Reviewed-on: AkkomaGang/pleroma-fe#202
2022-11-12 15:33:57 +00:00
Weblate
1157396ed5 Translated using Weblate (Japanese (ja_PEDANTIC))
Currently translated at 99.5% (1020 of 1025 strings)

Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ja_PEDANTIC/
Translation: Pleroma fe/pleroma-fe
2022-11-12 15:33:13 +00:00
Weblate
5a76dd5f90 Translated using Weblate (Spanish)
Currently translated at 85.7% (874 of 1019 strings)

Translated using Weblate (Spanish)

Currently translated at 85.7% (874 of 1019 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mint <they@mint.lgbt>
Co-authored-by: taretka <info@tarteka.net>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/es/
Translation: Pleroma fe/pleroma-fe
2022-11-12 15:33:13 +00:00
c7200b2234 make emoji reactions in notifications bar wider 2022-11-11 16:47:25 +00:00
98074ed90d make desc overflow a scroll 2022-11-11 16:20:41 +00:00
d51308a56b Merge pull request 'Set max-height for description container' (#201) from xarvos/pleroma-fe:fix-overflowing-description into develop
Reviewed-on: AkkomaGang/pleroma-fe#201
2022-11-11 16:19:59 +00:00
4b5536ae68 Merge branch 'develop' of akkoma.dev:AkkomaGang/pleroma-fe into develop 2022-11-11 16:05:10 +00:00
e44462b1d5 add "no reports" message 2022-11-11 16:05:02 +00:00
af32f901ac
Set max-height for description container
Without max-height, the description text can overflow the image, making
it impossible to close.

See issue #199: AkkomaGang/pleroma-fe#199
2022-11-11 11:33:21 +07:00
0810c57c8b Merge pull request 'Hide bubble timeline icon from desktop nav if empty' (#197) from sfr/pleroma-fe:fix/navbar-hide-bubble into develop
Reviewed-on: AkkomaGang/pleroma-fe#197
2022-11-10 12:10:05 +00:00
e77931d68c Use correct sensitiveIfSubject key 2022-11-10 12:06:59 +00:00
f3962e3be7 Merge pull request 'Add "requested to follow you" text on card' (#200) from inbound-requesting into develop
Reviewed-on: AkkomaGang/pleroma-fe#200
2022-11-10 03:16:17 +00:00
1e8fc5bcc4 Add "requested to follow you" text on card 2022-11-10 03:01:20 +00:00
Sol Fisher Romanoff
642fe3dc10
Hide bubble timeline icon from desktop nav if empty 2022-11-08 18:50:42 +02:00
8713f1870f use correct key for languages
fixes #196
2022-11-08 12:25:47 +00:00
837c61569a Merge pull request 'Fix profile field deletion' (#195) from sn0w/pleroma-fe:fix-profile-field-deletion into develop
Reviewed-on: AkkomaGang/pleroma-fe#195
2022-11-07 14:00:43 +00:00
a83c3a1fa1
Fix profile field deletion 2022-11-07 14:53:13 +01:00
Weblate
4d8f288bd9 Merge branch 'origin/develop' into Weblate. 2022-11-06 22:52:26 +00:00
3286641f3c Add software info on hover (#194)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/pleroma-fe#194
2022-11-06 22:52:25 +00:00
Weblate
677f5ae071 Merge branch 'origin/develop' into Weblate. 2022-11-06 21:26:06 +00:00
sfr
15bac1e401 Add reports management (#186)
implements part of #178, other parts will come later

Co-authored-by: Sol Fisher Romanoff <sol@solfisher.com>
Reviewed-on: AkkomaGang/pleroma-fe#186
Co-authored-by: sfr <sol@solfisher.com>
Co-committed-by: sfr <sol@solfisher.com>
2022-11-06 21:26:05 +00:00
Weblate
22b4aed8f6 Merge branch 'origin/develop' into Weblate. 2022-11-06 20:59:46 +00:00
23b0b01829 Merge pull request 'Highlight emoji reactions picked by yourself' (#193) from sfr/pleroma-fe:fix/emoji-reaction-picked into develop
Reviewed-on: AkkomaGang/pleroma-fe#193
2022-11-06 20:59:44 +00:00
Weblate
53c487535e Merge branch 'origin/develop' into Weblate. 2022-11-06 20:57:51 +00:00
278b2c25ad Merge pull request 'Clear search bar on reopen' (#192) from sfr/pleroma-fe:clear-searchbar into develop
Reviewed-on: AkkomaGang/pleroma-fe#192
2022-11-06 20:57:49 +00:00
Sol Fisher Romanoff
6a045dbc58
Highlight emoji reactions picked by yourself 2022-11-06 16:12:08 +02:00
Sol Fisher Romanoff
cd9dc9d2b2
Clear search bar on reopen 2022-11-06 15:46:35 +02:00
Weblate
3f2d54f057 Merge branch 'origin/develop' into Weblate. 2022-11-02 22:34:38 +00:00
251e440dad Merge pull request 'Ensure MFM scaling is ignored when rendering is disabled' (#189) from ignore-scale-mfm-disabled into develop
Reviewed-on: AkkomaGang/pleroma-fe#189
2022-11-02 22:34:36 +00:00
ffac376b5a Ensure MFM scaling is ignored when rendering is disabled
Fixes #173
2022-11-02 22:33:54 +00:00
Weblate
8bd18643e4 Translated using Weblate (French)
Currently translated at 100.0% (997 of 997 strings)

Co-authored-by: Thomate <thomas@burdick.fr>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/fr/
Translation: Pleroma fe/pleroma-fe
2022-11-02 22:09:52 +00:00
Weblate
5c28865018 Translated using Weblate (Catalan)
Currently translated at 100.0% (997 of 997 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: sola <spla@mastodont.cat>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ca/
Translation: Pleroma fe/pleroma-fe
2022-11-02 22:09:52 +00:00
721e3b016d Merge pull request 'Debounce word filters' (#188) from debounce-wordfilter into develop
Reviewed-on: AkkomaGang/pleroma-fe#188
2022-11-02 22:09:46 +00:00
469063ff52 Debounce word filters
Fixes #182
2022-11-02 22:08:58 +00:00
d8643b5b4a Take instance stopGifs value 2022-10-29 22:08:25 +01:00
04c744e764 Stop stopGifs option from instance 2022-10-29 21:56:58 +01:00
bda433b006 Add mfm autocomplete (#183)
I thought it could be neat to have an autocomplete like Misskey has for MFM.

A condition was removed that prevented autocomplete to actually autocomplete stuff when only the first character was entered. It doesn't affect the other autocompletes since none of them display their elements if nothing was actually searched. (in that case MFM returns the full list of elements)

Co-authored-by: solidsanek <solidsanek@outerheaven.club>
Reviewed-on: AkkomaGang/pleroma-fe#183
Reviewed-by: floatingghost <hannah@coffee-and-dreams.uk>
Co-authored-by: solidsanek <solidsanek@noreply.akkoma>
Co-committed-by: solidsanek <solidsanek@noreply.akkoma>
2022-10-29 20:50:31 +00:00
c8c8d40827 Merge pull request '2022.10 stable' (#177) from develop into stable
Reviewed-on: AkkomaGang/pleroma-fe#177
2022-10-08 11:13:01 +00:00
2e00c19074 Merge remote-tracking branch 'origin/translations' into develop 2022-10-08 11:56:09 +01:00
7df49720de ensure we sync settings whilst tab is open 2022-10-08 11:55:48 +01:00
Weblate
ddc40f5bb3 Translated using Weblate (Japanese (ja_PEDANTIC))
Currently translated at 100.0% (996 of 996 strings)

Translated using Weblate (Japanese (ja_PEDANTIC))

Currently translated at 100.0% (974 of 974 strings)

Translated using Weblate (Japanese (ja_PEDANTIC))

Currently translated at 100.0% (974 of 974 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ja_PEDANTIC/
Translation: Pleroma fe/pleroma-fe
2022-10-08 10:55:25 +00:00
Weblate
90793281d5 Translated using Weblate (French)
Currently translated at 97.8% (953 of 974 strings)

Translated using Weblate (French)

Currently translated at 97.7% (952 of 974 strings)

Translated using Weblate (French)

Currently translated at 96.9% (882 of 910 strings)

Co-authored-by: Thomate <thomas@burdick.fr>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/fr/
Translation: Pleroma fe/pleroma-fe
2022-10-08 10:55:25 +00:00
Weblate
d1dd043cfa Translated using Weblate (Dutch)
Currently translated at 100.0% (974 of 974 strings)

Translated using Weblate (Dutch)

Currently translated at 98.6% (961 of 974 strings)

Translated using Weblate (Dutch)

Currently translated at 95.3% (868 of 910 strings)

Co-authored-by: Fristi <fristi@subcon.town>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/nl/
Translation: Pleroma fe/pleroma-fe
2022-10-08 10:55:25 +00:00
Weblate
8745317c38 Translated using Weblate (German)
Currently translated at 98.1% (956 of 974 strings)

Co-authored-by: Johann <johann@qwertqwefsday.eu>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/de/
Translation: Pleroma fe/pleroma-fe
2022-10-08 10:55:25 +00:00
Sean King
80f58baa86 Change "Remove this follower" to "Remove Follower" and add a button to remove a follower in the followers tab for the logged in user 2022-10-06 17:07:44 +01:00
Sean King
2453a338be Added support for removing users from followers 2022-10-06 17:06:53 +01:00
4f837f75ea setting-sync (#175)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/pleroma-fe#175
2022-10-06 15:59:16 +00:00
eaf2bd05a0 Merge pull request 'Add delete & redraft button to posts' (#174) from sfr/pleroma-fe:feat/redraft into develop
Reviewed-on: AkkomaGang/pleroma-fe#174
2022-10-02 12:02:02 +00:00
Sol Fisher Romanoff
ca646822f6
Add delete & redraft button to posts 2022-09-25 20:50:03 +03:00
468eb12573 Detect whether an WEBP or APNG is animated (#161)
Co-authored-by: David <dmgf2008@hotmail.com>
Reviewed-on: AkkomaGang/pleroma-fe#161
Co-authored-by: Mergan <mergan@noreply.akkoma>
Co-committed-by: Mergan <mergan@noreply.akkoma>
2022-09-23 10:27:14 +00:00
2ab223e791 Merge pull request 'add hacky reply filtering, turn off streaming by default' (#172) from streaming-filters into develop
Reviewed-on: AkkomaGang/pleroma-fe#172
2022-09-20 15:28:53 +00:00
87683052e8 add hacky reply filtering, turn off streaming by default 2022-09-20 16:27:17 +01:00
42e48348ae add note about bubble timeline in docs 2022-09-20 15:17:51 +01:00
38d50acaeb Merge pull request 'Hide mentions of bubble timeline if empty' (#167) from sfr/pleroma-fe:hide-bubble into develop
Reviewed-on: AkkomaGang/pleroma-fe#167
2022-09-20 14:16:02 +00:00
e72548ae51 Merge pull request 'Move remote interaction chnagelog entry to "Unreleased"' (#171) from norm/pleroma-fe:remote-interaction-changelog into develop
Reviewed-on: AkkomaGang/pleroma-fe#171
2022-09-20 13:55:46 +00:00
bf1debdeb6
Move remote interaction chnagelog entry to "Unreleased" 2022-09-19 18:39:36 -04:00
aedd0794a4 Remote interaction with posts (#168)
From: https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1419

This is the frontend side of AkkomaGang/akkoma#198 (merged in a6d85003fe).

Co-authored-by: Tusooa Zhu <tusooa@kazv.moe>
Reviewed-on: AkkomaGang/pleroma-fe#168
Co-authored-by: Norm <normandy@biribiri.dev>
Co-committed-by: Norm <normandy@biribiri.dev>
2022-09-19 17:39:21 +00:00
92927bb7e6 Merge pull request 'Update 'package.json'' (#170) from norm/pleroma-fe:package.json into develop
Reviewed-on: AkkomaGang/pleroma-fe#170
2022-09-19 17:35:06 +00:00
7328795331 Update 'CHANGELOG.md' (#169)
Added in all of the major changes that were mentioned in the various release notes that weren't added to the changelog file.

Fixes #91

Reviewed-on: AkkomaGang/pleroma-fe#169
Co-authored-by: Norm <normandy@biribiri.dev>
Co-committed-by: Norm <normandy@biribiri.dev>
2022-09-19 17:32:45 +00:00
0a6c1f7d0a Update 'package.json'
The version and description in package.json was really out of date for some reason.
2022-09-18 04:29:04 +00:00
Sol Fisher Romanoff
b936506f47
Hide mentions of bubble timeline if empty 2022-09-14 21:44:29 +03:00
f628483499 translation parameterisation (#165)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/pleroma-fe#165
2022-09-12 15:47:53 +00:00
61c70545f0 Merge pull request 'Delete '.gitlab-ci.yml'' (#162) from norm/pleroma-fe:delete-gitlab-ci into develop
Reviewed-on: AkkomaGang/pleroma-fe#162
2022-09-11 19:17:42 +00:00
d7499a1f91 Merge pull request '2022.09 stable' (#160) from develop into stable
Reviewed-on: AkkomaGang/pleroma-fe#160
2022-09-10 14:39:13 +00:00
ad8be6b199 Merge branch 'develop' into delete-gitlab-ci 2022-09-10 04:03:58 +00:00
dd89735008 Delete '.gitlab-ci.yml'
This file obviously doesn't serve a purpose since we're not using Gitlab CI.
2022-09-09 23:05:10 +00:00
Weblate
3c780adeb2 Translated using Weblate (Japanese (ja_PEDANTIC))
Currently translated at 100.0% (910 of 910 strings)

Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ja_PEDANTIC/
Translation: Pleroma fe/pleroma-fe
2022-09-08 10:18:49 +00:00
Weblate
d813a8528a Translated using Weblate (French)
Currently translated at 92.6% (843 of 910 strings)

Translated using Weblate (French)

Currently translated at 92.6% (843 of 910 strings)

Co-authored-by: Thomate <thomas@burdick.fr>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/fr/
Translation: Pleroma fe/pleroma-fe
2022-09-08 10:18:49 +00:00
Weblate
58f6dfebe9 Translated using Weblate (English)
Currently translated at 100.0% (910 of 910 strings)

Co-authored-by: Thomate <thomas@burdick.fr>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/en/
Translation: Pleroma fe/pleroma-fe
2022-09-08 10:18:49 +00:00
da4b315d69 Merge remote-tracking branch 'origin/translations' into develop 2022-09-08 11:17:40 +01:00
b980d5b2ef use with-direction in edited timeago
Fixes #159
2022-09-07 12:55:03 +01:00
Weblate
9447b7eaea Merge branch 'origin/develop' into Weblate. 2022-09-06 19:25:07 +00:00
2da92fcd13 editing (#158)
Co-authored-by: Sean King <seanking2919@protonmail.com>
Co-authored-by: Tusooa Zhu <tusooa@kazv.moe>
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/pleroma-fe#158
2022-09-06 19:25:03 +00:00
Weblate
6aadbcecfa Translated using Weblate (Japanese (ja_PEDANTIC))
Currently translated at 100.0% (903 of 903 strings)

Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ja_PEDANTIC/
Translation: Pleroma fe/pleroma-fe
2022-09-06 17:00:17 +00:00
6084cbbb00 collapse statuses with CWs by default (#157)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/pleroma-fe#157
2022-09-06 17:00:15 +00:00
bd868e47de don't use bare @ 2022-09-06 11:44:53 +01:00
1cce6969cc make linter happy 2022-09-06 11:38:23 +01:00
Weblate
ebdcb31c12 Translated using Weblate (Japanese (ja_PEDANTIC))
Currently translated at 100.0% (903 of 903 strings)

Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ja_PEDANTIC/
Translation: Pleroma fe/pleroma-fe
2022-09-06 10:35:03 +00:00
Weblate
d42b207b64 Translated using Weblate (English)
Currently translated at 100.0% (903 of 903 strings)

Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/en/
Translation: Pleroma fe/pleroma-fe
2022-09-06 10:30:23 +00:00
Weblate
4f1d85b054 Translated using Weblate (French)
Currently translated at 94.9% (857 of 903 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/fr/
Translation: Pleroma fe/pleroma-fe
2022-09-05 17:24:25 +00:00
Weblate
cffc073aa7 Translated using Weblate (Japanese (ja_PEDANTIC))
Currently translated at 90.2% (816 of 904 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ja_PEDANTIC/
Translation: Pleroma fe/pleroma-fe
2022-09-05 17:24:25 +00:00
Weblate
d6bbbea5dc Translated using Weblate (Slovak)
Currently translated at 41.4% (374 of 903 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/sk/
Translation: Pleroma fe/pleroma-fe
2022-09-05 17:24:25 +00:00
Weblate
b5f90a96cc Translated using Weblate (Dutch)
Currently translated at 95.7% (865 of 903 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/nl/
Translation: Pleroma fe/pleroma-fe
2022-09-05 17:24:25 +00:00
Weblate
9220c3859a Translated using Weblate (Catalan)
Currently translated at 94.9% (857 of 903 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ca/
Translation: Pleroma fe/pleroma-fe
2022-09-05 17:24:25 +00:00
Weblate
aecb9f6e2a Translated using Weblate (English)
Currently translated at 99.5% (900 of 904 strings)

Translated using Weblate (English)

Currently translated at 99.5% (900 of 904 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/en/
Translation: Pleroma fe/pleroma-fe
2022-09-05 17:24:25 +00:00
ae1b6ad887 Customisation of default profile tab, clean up settings (#156)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/pleroma-fe#156
2022-09-05 17:02:16 +00:00
c301daa276 Merge pull request 'Jump to Top on New *only* in non-profiles' (#154) from Mergan/pleroma-fe:develop into develop
Reviewed-on: AkkomaGang/pleroma-fe#154
2022-09-05 00:31:06 +00:00
8e2a4a9f7b remove unused method 2022-09-05 01:30:32 +01:00
David
948436ad3e Jump to Top on New *only* in non-profiles 2022-09-02 17:05:42 -07:00
301a32991b Merge pull request 'Fix Announcements Title Style' (#152) from Mergan/pleroma-fe:develop into develop
Reviewed-on: AkkomaGang/pleroma-fe#152
2022-09-02 09:25:08 +00:00
David
91357aad88 Fix Announcements Title Style 2022-09-01 22:35:42 -07:00
c97cd56439 only clear timelines on new profile 2022-08-31 23:32:02 +01:00
Weblate
49875ef601 Translated using Weblate (Dutch)
Currently translated at 96.7% (868 of 897 strings)

Co-authored-by: Fristi <fristi@subcon.town>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/nl/
Translation: Pleroma fe/pleroma-fe
2022-08-31 16:45:29 +00:00
Weblate
a234cd3f00 Translated using Weblate (Japanese (ja_PEDANTIC))
Currently translated at 89.3% (806 of 902 strings)

Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ja_PEDANTIC/
Translation: Pleroma fe/pleroma-fe
2022-08-31 16:45:29 +00:00
6f2058a8fc ensure timelines only start fetching on click (#150)
no more parallel fetching

Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/pleroma-fe#150
2022-08-31 15:44:58 +00:00
064b8ba7f7 Merge pull request 'Make "Copy link" copy external URL if external post' (#144) from eris/pleroma-fe:copy-url-fix into develop
Reviewed-on: AkkomaGang/pleroma-fe#144
2022-08-31 15:43:37 +00:00
48826ede36 Separated Posts & Replies (#145) (#148)
Co-authored-by: David <dmgf2008@hotmail.com>
Reviewed-on: AkkomaGang/pleroma-fe#148
Co-authored-by: Mergan <mergan@noreply.akkoma>
Co-committed-by: Mergan <mergan@noreply.akkoma>
2022-08-31 15:28:09 +00:00
7dc0464094 split translation language by source and dest 2022-08-30 17:46:54 +01:00
6440e51b7e allow translating to any other language 2022-08-30 15:43:57 +01:00
ef50c63dc7 use supported languages from service 2022-08-30 14:37:36 +01:00
59eb434840 allow modal to expand with content 2022-08-30 10:32:34 +01:00
bd4e8271df Dont copy local URL if external post 2022-08-30 08:54:08 +00:00
08d4b3b885 Merge pull request 'allow selecting languages for translation' (#143) from translation-language-selection into develop
Reviewed-on: AkkomaGang/pleroma-fe#143
2022-08-29 21:17:41 +00:00
e7c21ffbd7 allow selecting languages for translation 2022-08-29 22:16:59 +01:00
9b4cddd2e9 Merge pull request 'Add ability to translate statuses' (#142) from machine-translation into develop
Reviewed-on: AkkomaGang/pleroma-fe#142
2022-08-29 19:41:50 +00:00
cd20e45157 Merge pull request 'Fix MRF transparency panel showing without transparency enabled' (#141) from eris/pleroma-fe:mrf-panel-fix into develop
Reviewed-on: AkkomaGang/pleroma-fe#141
2022-08-29 19:29:57 +00:00
48a11cb9d1 add translation options 2022-08-29 20:02:34 +01:00
e67d94f73d Fix MRF transparency panel showing without transparency 2022-08-28 10:09:56 +00:00
Weblate
b39143413c Translated using Weblate (Catalan)
Currently translated at 100.0% (860 of 860 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: sola <spla@mastodont.cat>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ca/
Translation: Pleroma fe/pleroma-fe
2022-08-26 12:44:30 +00:00
Weblate
b1460a9909 Translated using Weblate (Japanese (ja_PEDANTIC))
Currently translated at 86.1% (773 of 897 strings)

Translated using Weblate (Japanese (ja_PEDANTIC))

Currently translated at 86.0% (740 of 860 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ja_PEDANTIC/
Translation: Pleroma fe/pleroma-fe
2022-08-26 12:44:30 +00:00
776bee889e Confirmation dialogs (#140)
supercedes #135

adapted from https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1431

Co-authored-by: Tusooa Zhu <tusooa@kazv.moe>
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/pleroma-fe#140
2022-08-26 11:58:33 +00:00
f925fa6265 don't force white globe 2022-08-26 10:36:21 +01:00
c1c40d72da resolve language from navigator langs (#139)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/pleroma-fe#139
2022-08-26 09:31:25 +00:00
df70b1e4be Merge pull request 'Potential Fix for (#113) on show new' (#136) from Mergan/pleroma-fe:pleroma-fe#113 into develop
Reviewed-on: AkkomaGang/pleroma-fe#136
2022-08-25 19:41:10 +00:00
Weblate
58614fed35 Merge branch 'origin/develop' into Weblate. 2022-08-25 16:13:05 +00:00
05d052212a Merge pull request 'allow instance language to take precedence over EN' (#137) from default-language into develop
Reviewed-on: AkkomaGang/pleroma-fe#137
2022-08-25 16:13:02 +00:00
d4e91ef61e allow instance language to take precedence over EN 2022-08-25 17:02:55 +01:00
David
bf501e36a1 Potential Fix for (#113) on show new 2022-08-24 23:53:16 -07:00
Weblate
9a37c42f06 Translated using Weblate (French)
Currently translated at 100.0% (860 of 860 strings)

Co-authored-by: Thomate <thomas@burdick.fr>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/fr/
Translation: Pleroma fe/pleroma-fe
2022-08-23 15:12:20 +00:00
Weblate
f67868b9ad Translated using Weblate (Dutch)
Currently translated at 100.0% (860 of 860 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (859 of 859 strings)

Co-authored-by: Fristi <fristi@subcon.town>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/nl/
Translation: Pleroma fe/pleroma-fe
2022-08-23 15:12:19 +00:00
Weblate
e678327526 Translated using Weblate (Japanese (ja_PEDANTIC))
Currently translated at 86.0% (740 of 860 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Weblate Admin <hannah.ward9001@gmail.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ja_PEDANTIC/
Translation: Pleroma fe/pleroma-fe
2022-08-23 15:12:18 +00:00
Weblate
e1e3b9beff Translated using Weblate (Spanish)
Currently translated at 84.1% (724 of 860 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mint <they@mint.lgbt>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/es/
Translation: Pleroma fe/pleroma-fe
2022-08-23 15:12:18 +00:00
1f2b059320 add language tags 2022-08-23 16:12:06 +01:00
5972d89117 Merge pull request 'stable release' (#130) from develop into stable
Reviewed-on: AkkomaGang/pleroma-fe#130
2022-08-12 15:26:51 +00:00
d03872d598 Merge pull request 'port MFM link into stable docs' (#38) from develop into stable
Reviewed-on: AkkomaGang/pleroma-fe#38
2022-07-15 13:02:06 +00:00
193 changed files with 9431 additions and 7311 deletions

View file

@ -1,17 +1,17 @@
module.exports = { module.exports = {
root: true, root: true,
parserOptions: { parserOptions: {
parser: 'babel-eslint', parser: '@babel/eslint-parser',
sourceType: 'module' sourceType: 'module'
}, },
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: [ extends: [
'standard',
'plugin:vue/recommended' 'plugin:vue/recommended'
], ],
// required to lint *.vue files // required to lint *.vue files
plugins: [ plugins: [
'vue' 'vue',
'import'
], ],
// add your custom rules here // add your custom rules here
rules: { rules: {
@ -23,6 +23,8 @@ module.exports = {
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'vue/require-prop-types': 0, 'vue/require-prop-types': 0,
'vue/no-unused-vars': 0, 'vue/no-unused-vars': 0,
'no-tabs': 0 'no-tabs': 0,
'vue/multi-word-component-names': 0,
'vue/no-reserved-component-names': 0
} }
} }

View file

@ -0,0 +1,49 @@
name: "Bug report"
about: "Something isn't working as expected"
title: "[bug] "
body:
- type: markdown
attributes:
value: "Thanks for taking the time to file this bug report! Please try to be as specific and detailed as you can, so we can track down the issue and fix it as soon as possible."
- type: input
id: version
attributes:
label: "Version"
description: "Which version of pleroma-fe are you running? If running develop, specify the commit hash."
placeholder: "e.g. 2022.11, 40e86998e6"
- type: textarea
id: attempt
attributes:
label: "What were you trying to do?"
validations:
required: true
- type: textarea
id: expectation
attributes:
label: "What did you expect to happen?"
validations:
required: true
- type: textarea
id: reality
attributes:
label: "What actually happened?"
validations:
required: true
- type: dropdown
id: severity
attributes:
label: "Severity"
description: "Does this issue prevent you from using the software as normal?"
options:
- "I cannot use the software"
- "I cannot use it as easily as I'd like"
- "I can manage"
validations:
required: true
- type: checkboxes
id: searched
attributes:
label: "Have you searched for this issue?"
description: "Please double-check that your issue is not already being tracked on [the forums](https://meta.akkoma.dev) or [the issue tracker](https://akkoma.dev/AkkomaGang/pleroma-fe/issues)."
options:
- label: "I have double-checked and have not found this issue mentioned anywhere."

View file

@ -0,0 +1,29 @@
name: "Feature request"
about: "I'd like something to be added to pleroma-fe"
title: "[feat] "
body:
- type: markdown
attributes:
value: "Thanks for taking the time to request a new feature! Please be as concise and clear as you can in your proposal, so we could understand what you're going for."
- type: textarea
id: idea
attributes:
label: "The idea"
description: "What do you think you should be able to do in pleroma-fe?"
validations:
required: true
- type: textarea
id: reason
attributes:
label: "The reasoning"
description: "Why would this be a worthwhile feature? Does it solve any problems? Have people talked about wanting it?"
validations:
required: true
- type: checkboxes
id: searched
attributes:
label: "Have you searched for this feature request?"
description: "Please double-check that your issue is not already being tracked on [the forums](https://meta.akkoma.dev), [the issue tracker](https://akkoma.dev/AkkomaGang/pleroma-fe/issues), or the one for [the backend](https://akkoma.dev/AkkomaGang/akkoma/issues)."
options:
- label: "I have double-checked and have not found this feature request mentioned anywhere."
- label: "This feature is related to the pleroma-fe Akkoma frontend specifically, and not the backend."

1
.gitignore vendored
View file

@ -9,3 +9,4 @@ selenium-debug.log
config/local.json config/local.json
config/local.*.json config/local.*.json
docs/site/ docs/site/
.vscode/

View file

@ -1,47 +0,0 @@
# This file is a template, and might need editing before it works on your project.
# Official framework image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/node/tags/
image: node:12
stages:
- lint
- build
- test
- deploy
lint:
stage: lint
script:
- yarn
- npm run lint
- npm run stylelint
test:
stage: test
variables:
APT_CACHE_DIR: apt-cache
script:
- mkdir -pv $APT_CACHE_DIR && apt-get -qq update
- apt install firefox-esr -y --no-install-recommends
- firefox --version
- yarn
- yarn unit
build:
stage: build
script:
- yarn
- npm run build
artifacts:
paths:
- dist/
docs-deploy:
stage: deploy
image: alpine:latest
only:
- develop@pleroma/pleroma-fe
before_script:
- apk add curl
script:
- curl -X POST -F"token=$DOCS_PIPELINE_TRIGGER" -F'ref=master' https://git.pleroma.social/api/v4/projects/673/trigger/pipeline

View file

@ -1,19 +1,13 @@
{ {
"extends": [ "extends": [
"stylelint-rscss/config", "stylelint-config-recommended-vue/scss",
"stylelint-config-recommended", "stylelint-config-recommended",
"stylelint-config-standard" "stylelint-config-standard"
], ],
"customSyntax": "postcss-scss",
"rules": { "rules": {
"declaration-no-important": true, "declaration-no-important": true,
"rscss/no-descendant-combinator": false, "selector-class-pattern": null,
"rscss/class-format": [ "custom-property-pattern": null
true,
{
"component": "pascal-case",
"variant": "^-[a-z]\\w+",
"element": "^[a-z]\\w+"
}
]
} }
} }

View file

@ -3,17 +3,17 @@ pipeline:
when: when:
event: event:
- pull_request - pull_request
image: node:16 image: node:18
commands: commands:
- yarn - yarn
- yarn lint - yarn lint
- yarn stylelint #- yarn stylelint
test: test:
when: when:
event: event:
- pull_request - pull_request
image: node:16 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
@ -27,7 +27,7 @@ pipeline:
branch: branch:
- develop - develop
- stable - stable
image: node:16 image: node:18
commands: commands:
- yarn - yarn
- yarn build - yarn build
@ -39,7 +39,7 @@ pipeline:
branch: branch:
- develop - develop
- stable - stable
image: node:16 image: node:18
secrets: secrets:
- SCW_ACCESS_KEY - SCW_ACCESS_KEY
- SCW_SECRET_KEY - SCW_SECRET_KEY

View file

@ -4,6 +4,33 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased ## Unreleased
### Added
- Implemented remote interaction with statuses
## 2022.09 - 2022-09-10
### Added
- Automatic post translations. Must be configured on the backend in order to work.
- Post editing, including a log of previous edits.
### Changed
- Top bar now has navigation shortcuts. Can be enabled or disabled by admins or users.
- Optional replacement of timeline drop-down with navigation buttons. Also configurable.
- Posts and posts with replies are now separated on user profiles.
- Custom emoji from remote instances on a post can now also be used.
## 2022.08 - 2022-08-12
### Added
- Ability to quote public and unlisted posts
- Bubble timeline
### Changed
- Emoji in emoji picker is separated by packs
### Removed
- Chats, they were half-baked. Just use PMs.
## 2022.07 - 2022-07-16
### Fixed ### Fixed
- AdminFE button no longer scrolls page to top when clicked - AdminFE button no longer scrolls page to top when clicked
- Pinned statuses no longer appear at bottom of user timeline (still appear as part of the timeline when fetched deep enough) - Pinned statuses no longer appear at bottom of user timeline (still appear as part of the timeline when fetched deep enough)
@ -16,6 +43,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Attachments are ALWAYS in same order as user uploaded, no more "videos first" - Attachments are ALWAYS in same order as user uploaded, no more "videos first"
- Attachment description is prefilled with backend-provided default when uploading - Attachment description is prefilled with backend-provided default when uploading
- Proper visual feedback that next image is loading when browsing - Proper visual feedback that next image is loading when browsing
- Misskey-Flavoured Markdown support
- Custom emoji reactions
### Changed ### Changed
- (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out) - (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out)

24
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,24 @@
# Akkoma Code of Conduct
The Akkoma project aims to be **enjoyable** for anyone to participate in, regardless of their identity or level of expertise. To achieve this, the community must create an environment which is **safe** and **equitable**; the following guidelines have been created with these goals in mind.
1. **Treat individuals with respect.** Differing experiences and viewpoints deserve to be respected, and bigotry and harassment are not tolerated under any circumstances.
- Individuals should at all times be treated as equals, regardless of their age, gender, sexuality, race, ethnicity, _or any other characteristic_, intrinsic or otherwise.
- Behaviour that is harmful in nature should be addressed and corrected *regardless of intent*.
- Respect personal boundaries and ask for clarification whenever they are unclear.
- (Obviously, hate does not count as merely a "differing viewpoint", because it is harmful in nature.)
2. **Be understanding of differences in communication.** Not everyone is aware of unspoken social cues, and speech that is not intended to be offensive should not be treated as such simply due to an atypical manner of communication.
- Somebody who speaks bluntly is not necessarily rude, and somebody who swears a lot is not necessarily volatile.
- Try to confirm your interpretation of their intent rather than assuming bad faith.
- Someone may not communicate as, or come across as a picture of "professionalism", but this should not be seen as a reason to dismiss them. This is a **casual** space, and communication styles can reflect that.
3. **"Uncomfortable" does not mean "unsafe".** In an ideal world, the community would be safe, equitable, enjoyable, *and* comfortable for all members at all times. Unfortunately, this is not always possible in reality.
- Safety and equity will be prioritized over comfort whenever it is necessary to do so.
- Weaponizing one's own discomfort to deflect accountability or censor an individual (e.g. "white fragility") is a form of discriminatory conduct.
4. **Let people grow from their mistakes.** Nobody is perfect; even the most well-meaning individual can do something hurtful. Everyone should be given a fair opportunity to explain themselves and correct their behaviour. Portraying someone as inherently malicious prevents improvement and shifts focus away from the *action* that was problematic.
- Avoid bringing up past events that do not accurately reflect an individual's current actions or beliefs. (This is, of course, different from providing evidence of a recurring pattern of behaviour.)
---
This document was adapted from one created by ~keith as part of punks default repository template, and is licensed under CC-BY-SA 4.0. The original template is here: <https://bytes.keithhacks.cyou/keith/default-template>

View file

@ -1,49 +0,0 @@
```
o$$$$$$oo
o$" "$oo
$ o""""$o "$o
"$ o "o "o $
"$ $o $ $ o$
"$ o$"$ o$
"$ooooo$$ $ o$
o$ """ $ " $$$ " $
o$ $o $$" " "
$$ $ " $ $$$o"$ o o$"
$" o "" $ $" " o" $$
$o " " $ o$" o" o$"
"$o $$ $ o" o$$"
""o$o"$" $oo" o$"
o$$ $ $$$ o$$
o" o oo"" "" "$o
o$o" "" $
$" " o" " " " "o
$$ " " o$ o$o " $
o$ $ $ o$$ " " ""
o $ $" " "o o$
$ o $o$oo$""
$o $ o o o"$$
$o o $ $ "$o
$o $ o $ $ "o
$ $ "o $ "o"$o
$ " o $ o $$
$o$o$o$o$$o$$$o$$o$o$$o$$o$$$o$o$o$o$o$o$o$o$o$ooo
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ " $$$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ o$$$$"
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ooooo$$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"""""
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
"$o$o$o$o$o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
"$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
"$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"""
"""""""""""""""""""""""""""""""""""""""""""""""""""""
```

View file

@ -10,3 +10,5 @@ Contributors of this project.
- shpuld (shpuld@shitposter.club): CSS and styling - shpuld (shpuld@shitposter.club): CSS and styling
- Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images. - Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images.
- hj (hj@shigusegubu.club): Code - hj (hj@shigusegubu.club): Code
- Sean King (seanking@freespeechextremist.com): Code
- Tusooa Zhu (tusooa@kazv.moe): Code

View file

@ -1,20 +1,22 @@
# Pleroma-FE # Akkoma-FE
This is a fork of Pleroma-FE from the Pleroma project, with support for new Akkoma features such as: ![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)
This is a fork of Akkoma-FE from the Pleroma project, with support for new Akkoma features such as:
- 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
# For Translators # For Translators
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Pleroma-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.
Pleroma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js. Akkoma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js.
# FOR ADMINS # FOR ADMINS
To use Pleroma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/) CLI task to install Pleroma-FE, then modify your configuration as described in the [Frontend Management](https://docs.akkoma.dev/stable/configuration/frontend_management/) doc. To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/) CLI task to install Akkoma-FE, then modify your configuration as described in the [Frontend Management](https://docs.akkoma.dev/stable/configuration/frontend_management/) doc.
## Build Setup ## Build Setup

View file

@ -29,18 +29,6 @@ var devMiddleware = require('webpack-dev-middleware')(compiler, {
}) })
var hotMiddleware = require('webpack-hot-middleware')(compiler) var hotMiddleware = require('webpack-hot-middleware')(compiler)
// force page reload when html-webpack-plugin template changes
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
// FIXME: This supposed to reload whole page when index.html is changed,
// however now it reloads entire page on every breath, i suppose the order
// of plugins changed or something. It's a minor thing and douesn't hurt
// disabling it, constant reloads hurt much more
// hotMiddleware.publish({ action: 'reload' })
// cb()
})
})
// proxy api requests // proxy api requests
Object.keys(proxyTable).forEach(function (context) { Object.keys(proxyTable).forEach(function (context) {

View file

@ -2,8 +2,6 @@ var path = require('path')
var config = require('../config') var config = require('../config')
var utils = require('./utils') var utils = require('./utils')
var projectRoot = path.resolve(__dirname, '../') var projectRoot = path.resolve(__dirname, '../')
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
var CopyPlugin = require('copy-webpack-plugin');
var { VueLoaderPlugin } = require('vue-loader') var { VueLoaderPlugin } = require('vue-loader')
var env = process.env.NODE_ENV var env = process.env.NODE_ENV
@ -20,6 +18,7 @@ module.exports = {
app: './src/main.js' app: './src/main.js'
}, },
output: { output: {
hashFunction: "sha256", // Workaround for builds with OpenSSL 3.
path: config.build.assetsRoot, path: config.build.assetsRoot,
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath, publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
filename: '[name].js' filename: '[name].js'
@ -34,6 +33,9 @@ module.exports = {
modules: [ modules: [
path.join(__dirname, '../node_modules') path.join(__dirname, '../node_modules')
], ],
fallback: {
"url": require.resolve("url/"),
},
alias: { alias: {
'static': path.resolve(__dirname, '../static'), 'static': path.resolve(__dirname, '../static'),
'src': path.resolve(__dirname, '../src'), 'src': path.resolve(__dirname, '../src'),
@ -116,23 +118,6 @@ module.exports = {
] ]
}, },
plugins: [ plugins: [
new ServiceWorkerWebpackPlugin({ new VueLoaderPlugin()
entry: path.join(__dirname, '..', 'src/sw.js'),
filename: 'sw-pleroma.js'
}),
new VueLoaderPlugin(),
// This copies Ruffle's WASM to a directory so that JS side can access it
new CopyPlugin({
patterns: [
{
from: "node_modules/ruffle-mirror/*",
to: "static/ruffle",
flatten: true
},
],
options: {
concurrency: 100,
},
})
] ]
} }

View file

@ -1,6 +1,6 @@
var config = require('../config') var config = require('../config')
var webpack = require('webpack') var webpack = require('webpack')
var merge = require('webpack-merge') var { merge } = require('webpack-merge')
var utils = require('./utils') var utils = require('./utils')
var baseWebpackConfig = require('./webpack.base.conf') var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin') var HtmlWebpackPlugin = require('html-webpack-plugin')
@ -16,7 +16,7 @@ module.exports = merge(baseWebpackConfig, {
}, },
mode: 'development', mode: 'development',
// eval-source-map is faster for development // eval-source-map is faster for development
devtool: '#eval-source-map', devtool: 'eval-source-map',
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': config.dev.env, 'process.env': config.dev.env,

View file

@ -2,7 +2,8 @@ var path = require('path')
var config = require('../config') var config = require('../config')
var utils = require('./utils') var utils = require('./utils')
var webpack = require('webpack') var webpack = require('webpack')
var merge = require('webpack-merge') const WorkboxPlugin = require('workbox-webpack-plugin');
var { merge } = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf') var baseWebpackConfig = require('./webpack.base.conf')
var MiniCssExtractPlugin = require('mini-css-extract-plugin') var MiniCssExtractPlugin = require('mini-css-extract-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin') var HtmlWebpackPlugin = require('html-webpack-plugin')
@ -19,7 +20,7 @@ var webpackConfig = merge(baseWebpackConfig, {
module: { module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true }) rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true })
}, },
devtool: config.build.productionSourceMap ? '#source-map' : false, devtool: 'source-map',
optimization: { optimization: {
minimize: true, minimize: true,
splitChunks: { splitChunks: {
@ -32,6 +33,11 @@ var webpackConfig = merge(baseWebpackConfig, {
chunkFilename: utils.assetsPath('js/[name].[chunkhash].js') chunkFilename: utils.assetsPath('js/[name].[chunkhash].js')
}, },
plugins: [ plugins: [
new WorkboxPlugin.InjectManifest({
swSrc: path.join(__dirname, '..', 'src/sw.js'),
swDest: 'sw-pleroma.js',
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
}),
// http://vuejs.github.io/vue-loader/workflow/production.html // http://vuejs.github.io/vue-loader/workflow/production.html
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': env, 'process.env': env,
@ -62,7 +68,7 @@ var webpackConfig = merge(baseWebpackConfig, {
// https://github.com/kangax/html-minifier#options-quick-reference // https://github.com/kangax/html-minifier#options-quick-reference
}, },
// necessary to consistently work with multiple chunks via CommonsChunkPlugin // necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency' chunksSortMode: 'auto'
}), }),
// split vendor js into its own file // split vendor js into its own file
// extract webpack runtime and module manifest to its own file in order to // extract webpack runtime and module manifest to its own file in order to

View file

@ -1,4 +1,4 @@
var merge = require('webpack-merge') var { merge } = require('webpack-merge')
var prodEnv = require('./prod.env') var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, { module.exports = merge(prodEnv, {

4
config/ihba.json Normal file
View file

@ -0,0 +1,4 @@
{
"target": "https://ihatebeinga.live",
"staticConfigPreference": false
}

View file

@ -38,6 +38,11 @@ module.exports = {
assetsSubDirectory: 'static', assetsSubDirectory: 'static',
assetsPublicPath: '/', assetsPublicPath: '/',
proxyTable: { proxyTable: {
'/manifest.json': {
target,
changeOrigin: true,
cookieDomainRewrite: 'localhost'
},
'/api': { '/api': {
target, target,
changeOrigin: true, changeOrigin: true,

View file

@ -1,4 +1,4 @@
var merge = require('webpack-merge') var { merge } = require('webpack-merge')
var devEnv = require('./dev.env') var devEnv = require('./dev.env')
module.exports = merge(devEnv, { module.exports = merge(devEnv, {

View file

@ -70,9 +70,6 @@ Default post formatting option (markdown/bbcode/plaintext/etc...)
### `redirectRootNoLogin`, `redirectRootLogin` ### `redirectRootNoLogin`, `redirectRootLogin`
These two settings should point to where FE should redirect visitor when they login/open up website root These two settings should point to where FE should redirect visitor when they login/open up website root
### `scopeCopy`
Copy post scope (visibility) when replying to a post. Instance-default.
### `sidebarRight` ### `sidebarRight`
Change alignment of sidebar and panels to the right. Defaults to `false`. Change alignment of sidebar and panels to the right. Defaults to `false`.

View file

@ -6,6 +6,7 @@ You have several timelines to browse trough
- **Bookmarks** all the posts you've bookmarked. You can bookmark a post by clicking the three dots on the bottom right of the post and choose Bookmark. - **Bookmarks** all the posts you've bookmarked. You can bookmark a post by clicking the three dots on the bottom right of the post and choose Bookmark.
- **Direct Messages** all posts with `direct` scope addressed to you or mentioning you. - **Direct Messages** all posts with `direct` scope addressed to you or mentioning you.
- **Public Timelines** all public posts made by users on the instance you're on - **Public Timelines** all public posts made by users on the instance you're on
- **Bubble Timeline** all public posts from instances recommended by your admin(s) in the instance settings. This won't appear if they haven't set anything up for it.
- **The Whole Known Network** also known as **TWKN** or **Federated Timeline** - all public posts known by your instance. Due to nature of the network your instance may not know *all* the posts on the network, so only posts known by your instance are shown there. - **The Whole Known Network** also known as **TWKN** or **Federated Timeline** - all public posts known by your instance. Due to nature of the network your instance may not know *all* the posts on the network, so only posts known by your instance are shown there.
Note that by default you will see all posts made by other users on your Home Timeline, this contrast behavior of Twitter and Mastodon, which shows you only non-reply posts and replies to people you follow. You can change said behavior in the [settings](settings.md#filtering). Note that by default you will see all posts made by other users on your Home Timeline, this contrast behavior of Twitter and Mastodon, which shows you only non-reply posts and replies to people you follow. You can change said behavior in the [settings](settings.md#filtering).

View file

@ -3,18 +3,22 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
<title>Pleroma</title> <title>Akkoma</title>
<link rel="stylesheet" href="/static/font/css/fontello.css"> <link rel="stylesheet" href="/static/font/css/fontello.css">
<link rel="stylesheet" href="/static/font/css/animation.css"> <link rel="stylesheet" href="/static/font/css/animation.css">
<link rel="stylesheet" href="/static/font/tiresias.css"> <link rel="stylesheet" href="/static/font/tiresias.css">
<link rel="stylesheet" href="/static/font/css/lato.css"> <link rel="stylesheet" href="/static/font/css/lato.css">
<link rel="stylesheet" href="/static/mfm.css"> <link rel="stylesheet" href="/static/mfm.css">
<link rel="stylesheet" href="/static/custom.css">
<link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder">
<!--server-generated-meta--> <!--server-generated-meta-->
<link rel="icon" type="image/png" href="/favicon.png"> <link rel="icon" type="image/png" href="/favicon.png">
<link rel="manifest" href="/manifest.json">
</head> </head>
<body class="hidden"> <body class="hidden">
<noscript>To use Pleroma, please enable JavaScript.</noscript> <noscript>To use Akkoma, please enable JavaScript.</noscript>
<div id="app"></div> <div id="app"></div>
<div id="modal"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>
</html> </html>

View file

@ -1,7 +1,7 @@
{ {
"name": "pleroma_fe", "name": "pleroma_fe",
"version": "1.0.0", "version": "3.5.0",
"description": "A Qvitter-style frontend for certain GS servers.", "description": "A frontend for Akkoma instances",
"author": "Roger Braun <roger@rogerbraun.net>", "author": "Roger Braun <roger@rogerbraun.net>",
"private": true, "private": true,
"scripts": { "scripts": {
@ -11,43 +11,44 @@
"unit:watch": "karma start test/unit/karma.conf.js --single-run=false", "unit:watch": "karma start test/unit/karma.conf.js --single-run=false",
"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": "npx stylelint src/components/status/status.scss", "stylelint": "stylelint src/**/*.scss",
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs", "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
"lint-fix": "eslint --fix --ext .js,.vue 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",
"@fortawesome/fontawesome-svg-core": "1.3.0", "@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-regular-svg-icons": "^6.1.2", "@fortawesome/free-regular-svg-icons": "^6.1.2",
"@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/vue-fontawesome": "3.0.1", "@fortawesome/vue-fontawesome": "3.0.1",
"@kazvmoe-infra/pinch-zoom-element": "1.2.0", "@vuelidate/core": "^2.0.0",
"@vuelidate/core": "2.0.0-alpha.42", "@vuelidate/validators": "^2.0.0",
"@vuelidate/validators": "2.0.0-alpha.30", "blurhash": "^2.0.4",
"body-scroll-lock": "2.7.1", "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.5.12", "cropperjs": "1.5.12",
"diff": "3.5.0", "diff": "3.5.0",
"escape-html": "1.0.3", "escape-html": "1.0.3",
"iso-639-1": "^2.1.15",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"localforage": "1.10.0", "localforage": "1.10.0",
"marked": "^4.0.17", "parse-link-header": "^2.0.0",
"marked-mfm": "^0.5.0",
"parse-link-header": "1.0.1",
"phoenix": "1.6.2", "phoenix": "1.6.2",
"punycode.js": "2.1.0", "punycode.js": "2.1.0",
"qrcode": "1", "qrcode": "1",
"ruffle-mirror": "2021.12.31", "url": "^0.11.0",
"vue": "^3.2.31", "vue": "^3.2.31",
"vue-i18n": "^9.2.0-beta.39", "vue-i18n": "^9.2.2",
"vue-router": "4.0.14", "vue-router": "4.0.14",
"vue-template-compiler": "2.6.11", "vue-template-compiler": "2.6.11",
"vuex": "4.0.2" "vuex": "4.0.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.17.8", "@babel/core": "7.17.8",
"@babel/eslint-parser": "^7.19.1",
"@babel/plugin-transform-runtime": "7.17.0", "@babel/plugin-transform-runtime": "7.17.0",
"@babel/preset-env": "7.16.11", "@babel/preset-env": "7.16.11",
"@babel/register": "7.17.7", "@babel/register": "7.17.7",
@ -58,34 +59,31 @@
"@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": "6.7.7", "autoprefixer": "6.7.7",
"babel-eslint": "7.2.3", "babel-loader": "^9.1.0",
"babel-loader": "8.2.4",
"babel-plugin-lodash": "3.3.4", "babel-plugin-lodash": "3.3.4",
"chai": "3.5.0", "chai": "^4.3.7",
"chalk": "1.1.3", "chalk": "1.1.3",
"chromedriver": "87.0.7", "chromedriver": "^107.0.3",
"connect-history-api-fallback": "1.6.0", "connect-history-api-fallback": "^2.0.0",
"copy-webpack-plugin": "6.4.1", "cross-spawn": "^7.0.3",
"cross-spawn": "4.0.2", "css-loader": "^6.7.2",
"css-loader": "0.28.11", "custom-event-polyfill": "^1.0.7",
"custom-event-polyfill": "1.0.7", "eslint": "^7.32.0",
"eslint": "5.16.0", "eslint-config-standard": "^17.0.0",
"eslint-config-standard": "12.0.0", "eslint-friendly-formatter": "^4.0.1",
"eslint-friendly-formatter": "2.0.7", "eslint-loader": "^4.0.2",
"eslint-loader": "2.2.1", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-import": "2.25.4", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-node": "7.0.1", "eslint-plugin-promise": "^6.1.1",
"eslint-plugin-promise": "4.3.1", "eslint-plugin-standard": "^5.0.0",
"eslint-plugin-standard": "4.1.0", "eslint-plugin-vue": "^9.7.0",
"eslint-plugin-vue": "5.2.3",
"eventsource-polyfill": "0.9.6", "eventsource-polyfill": "0.9.6",
"express": "4.17.3", "express": "4.17.3",
"file-loader": "3.0.1", "file-loader": "^6.2.0",
"function-bind": "1.1.1", "function-bind": "1.1.1",
"html-webpack-plugin": "3.2.0", "html-webpack-plugin": "^5.5.0",
"http-proxy-middleware": "0.21.0", "http-proxy-middleware": "0.21.0",
"inject-loader": "2.0.1", "inject-loader": "2.0.1",
"iso-639-1": "2.1.15",
"isparta-loader": "2.0.0", "isparta-loader": "2.0.0",
"json-loader": "0.5.7", "json-loader": "0.5.7",
"karma": "6.3.17", "karma": "6.3.17",
@ -96,7 +94,7 @@
"karma-sinon-chai": "2.0.2", "karma-sinon-chai": "2.0.2",
"karma-sourcemap-loader": "0.3.8", "karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.33", "karma-spec-reporter": "0.0.33",
"karma-webpack": "4.0.2", "karma-webpack": "^5.0.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"lolex": "1.6.0", "lolex": "1.6.0",
"mini-css-extract-plugin": "0.12.0", "mini-css-extract-plugin": "0.12.0",
@ -104,29 +102,33 @@
"nightwatch": "0.9.21", "nightwatch": "0.9.21",
"opn": "4.0.2", "opn": "4.0.2",
"ora": "0.4.1", "ora": "0.4.1",
"postcss-html": "^1.5.0",
"postcss-loader": "3.0.0", "postcss-loader": "3.0.0",
"postcss-sass": "^0.5.0",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"sass": "1.53.0", "sass": "^1.56.0",
"sass-loader": "7.3.1", "sass-loader": "^13.2.0",
"selenium-server": "2.53.1", "selenium-server": "2.53.1",
"semver": "5.7.1", "semver": "5.7.1",
"serviceworker-webpack-plugin": "1.0.1",
"shelljs": "0.8.5", "shelljs": "0.8.5",
"sinon": "2.4.1", "sinon": "2.4.1",
"sinon-chai": "2.14.0", "sinon-chai": "2.14.0",
"stylelint": "13.6.1", "stylelint": "^14.15.0",
"stylelint-config-standard": "20.0.0", "stylelint-config-recommended-vue": "^1.4.0",
"stylelint-rscss": "0.4.0", "stylelint-config-standard": "^29.0.0",
"url-loader": "1.1.2", "stylelint-config-standard-scss": "^6.1.0",
"vue-loader": "^16.0.0", "stylelint-rscss": "^0.4.0",
"vue-style-loader": "4.1.2", "url-loader": "^4.1.1",
"webpack": "4.46.0", "vue-loader": "^17.0.0",
"webpack-dev-middleware": "3.7.3", "vue-style-loader": "^4.1.2",
"webpack-hot-middleware": "2.25.1", "webpack": "^5.75.0",
"webpack-merge": "0.20.0" "webpack-dev-middleware": "^5.3.3",
"webpack-hot-middleware": "^2.25.1",
"webpack-merge": "^5.8.0",
"workbox-webpack-plugin": "^6.5.4"
}, },
"engines": { "engines": {
"node": ">= 4.0.0", "node": ">= 16.0.0",
"npm": ">= 3.0.0" "npm": ">= 3.0.0"
} }
} }

View file

@ -5,12 +5,15 @@ import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import SettingsModal from './components/settings_modal/settings_modal.vue' import SettingsModal from './components/settings_modal/settings_modal.vue'
import MediaModal from './components/media_modal/media_modal.vue' import MediaModal from './components/media_modal/media_modal.vue'
import ModModal from './components/mod_modal/mod_modal.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue' import SideDrawer from './components/side_drawer/side_drawer.vue'
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue' import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
import MobileNav from './components/mobile_nav/mobile_nav.vue' import MobileNav from './components/mobile_nav/mobile_nav.vue'
import DesktopNav from './components/desktop_nav/desktop_nav.vue' import DesktopNav from './components/desktop_nav/desktop_nav.vue'
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue' import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
import PostStatusModal from './components/post_status_modal/post_status_modal.vue' import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue' import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
import { windowWidth, windowHeight } from './services/window_utils/window_utils' import { windowWidth, windowHeight } from './services/window_utils/window_utils'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
@ -31,8 +34,11 @@ export default {
MobileNav, MobileNav,
DesktopNav, DesktopNav,
SettingsModal, SettingsModal,
ModModal,
UserReportingModal, UserReportingModal,
PostStatusModal, PostStatusModal,
EditStatusModal,
StatusHistoryModal,
GlobalNoticeList GlobalNoticeList
}, },
data: () => ({ data: () => ({
@ -83,8 +89,10 @@ export default {
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile' return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
}, },
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
editingAvailable () { return this.$store.state.instance.editingAvailable },
layoutType () { return this.$store.state.interface.layoutType }, layoutType () { return this.$store.state.interface.layoutType },
privateMode () { return this.$store.state.instance.private }, privateMode () { return this.$store.state.instance.private },
showPublicExternalTimeline () {return currentUser() || !(privateMode || this.$store.state.instance.restrict_unauthenticated.timelines.federated) },
reverseLayout () { reverseLayout () {
const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig
if (this.layoutType !== 'wide') { if (this.layoutType !== 'wide') {

View file

@ -1,6 +1,7 @@
// stylelint-disable rscss/class-format // stylelint-disable rscss/class-format
@import './_variables.scss'; @import './_variables.scss';
@import '@fortawesome/fontawesome-svg-core/styles.css';
@import '@floatingghost/pinch-zoom-element/dist/pinch-zoom.css';
:root { :root {
--navbar-height: 3.5rem; --navbar-height: 3.5rem;
--post-line-height: 1.4; --post-line-height: 1.4;

View file

@ -58,8 +58,10 @@
<MobilePostStatusButton /> <MobilePostStatusButton />
<UserReportingModal /> <UserReportingModal />
<PostStatusModal /> <PostStatusModal />
<EditStatusModal v-if="editingAvailable" />
<StatusHistoryModal v-if="editingAvailable" />
<SettingsModal /> <SettingsModal />
<div id="modal" /> <ModModal />
<GlobalNoticeList /> <GlobalNoticeList />
</div> </div>
</template> </template>

View file

@ -1,8 +1,11 @@
import Cookies from 'js-cookie'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import vClickOutside from 'click-outside-vue3' import vClickOutside from 'click-outside-vue3'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome' import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
import { config } from '@fortawesome/fontawesome-svg-core';
config.autoAddCss = false
import App from '../App.vue' import App from '../App.vue'
import routes from './routes' import routes from './routes'
@ -48,6 +51,20 @@ const preloadFetch = async (request) => {
} }
} }
const resolveLanguage = (instanceLanguages) => {
// First language in navigator.languages that is listed as an instance language
// falls back to first instance language
const navigatorLanguages = navigator.languages.map((x) => x.split('-')[0])
for (const navLanguage of navigatorLanguages) {
if (instanceLanguages.includes(navLanguage)) {
return navLanguage
}
}
return instanceLanguages[0]
}
const getInstanceConfig = async ({ store }) => { const getInstanceConfig = async ({ store }) => {
try { try {
const res = await preloadFetch('/api/v1/instance') const res = await preloadFetch('/api/v1/instance')
@ -58,6 +75,10 @@ const getInstanceConfig = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit }) store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required }) store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
// don't override cookie if set
if (!Cookies.get('userLanguage')) {
store.dispatch('setOption', { name: 'interfaceLanguage', value: resolveLanguage(data.languages) })
}
if (vapidPublicKey) { if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
@ -129,7 +150,9 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('showWiderShortcuts') copyInstanceOption('showWiderShortcuts')
copyInstanceOption('showNavShortcuts') copyInstanceOption('showNavShortcuts')
copyInstanceOption('showPanelNavShortcuts') copyInstanceOption('showPanelNavShortcuts')
copyInstanceOption('stopGifs')
copyInstanceOption('logo') copyInstanceOption('logo')
copyInstanceOption('conversationDisplay')
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', {
name: 'logoMask', name: 'logoMask',
@ -150,10 +173,8 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('redirectRootNoLogin') copyInstanceOption('redirectRootNoLogin')
copyInstanceOption('redirectRootLogin') copyInstanceOption('redirectRootLogin')
copyInstanceOption('showInstanceSpecificPanel') copyInstanceOption('showInstanceSpecificPanel')
copyInstanceOption('minimalScopesMode')
copyInstanceOption('hideMutedPosts') copyInstanceOption('hideMutedPosts')
copyInstanceOption('collapseMessageWithSubject') copyInstanceOption('collapseMessageWithSubject')
copyInstanceOption('scopeCopy')
copyInstanceOption('subjectLineBehavior') copyInstanceOption('subjectLineBehavior')
copyInstanceOption('postContentType') copyInstanceOption('postContentType')
copyInstanceOption('alwaysShowSubjectInput') copyInstanceOption('alwaysShowSubjectInput')
@ -254,8 +275,10 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') }) store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
store.dispatch('setInstanceOption', { name: 'translationEnabled', value: features.includes('akkoma:machine_translation') })
const uploadLimits = metadata.uploadLimits const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
@ -375,6 +398,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
// Start fetching things that don't need to block the UI // Start fetching things that don't need to block the UI
store.dispatch('fetchMutes') store.dispatch('fetchMutes')
store.dispatch('startFetchingAnnouncements') store.dispatch('startFetchingAnnouncements')
store.dispatch('startFetchingReports')
getTOS({ store }) getTOS({ store })
getStickers({ store }) getStickers({ store })

View file

@ -22,6 +22,8 @@ import Lists from 'components/lists/lists.vue'
import ListTimeline from 'components/list_timeline/list_timeline.vue' import ListTimeline from 'components/list_timeline/list_timeline.vue'
import ListEdit from 'components/list_edit/list_edit.vue' import ListEdit from 'components/list_edit/list_edit.vue'
import AnnouncementsPage from 'components/announcements_page/announcements_page.vue' import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
import RegistrationRequestSent from 'components/registration_request_sent/registration_request_sent.vue'
import AwaitingEmailConfirmation from 'components/awaiting_email_confirmation/awaiting_email_confirmation.vue'
export default (store) => { export default (store) => {
const validateAuthenticatedRoute = (to, from, next) => { const validateAuthenticatedRoute = (to, from, next) => {
@ -62,6 +64,8 @@ export default (store) => {
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute }, { name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{ name: 'registration', path: '/registration', component: Registration }, { name: 'registration', path: '/registration', component: Registration },
{ name: 'registration-request-sent', path: '/registration-request-sent', component: RegistrationRequestSent },
{ name: 'awaiting-email-confirmation', path: '/awaiting-email-confirmation', component: AwaitingEmailConfirmation },
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true }, { name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
{ name: 'registration-token', path: '/registration/:token', component: Registration }, { name: 'registration-token', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },

View file

@ -20,6 +20,9 @@ const About = {
return this.$store.state.instance.showInstanceSpecificPanel && return this.$store.state.instance.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP && !this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent this.$store.state.instance.instanceSpecificPanelContent
},
showLocalBubblePanel () {
return this.$store.state.instance.localBubbleInstances.length > 0
} }
} }
} }

View file

@ -1,9 +1,9 @@
<template> <template>
<div class="column-inner"> <div class="column-inner">
<instance-specific-panel v-if="showInstanceSpecificPanel" /> <instance-specific-panel />
<staff-panel /> <staff-panel />
<terms-of-service-panel /> <terms-of-service-panel />
<LocalBubblePanel /> <LocalBubblePanel v-if="showLocalBubblePanel" />
<MRFTransparencyPanel /> <MRFTransparencyPanel />
<features-panel v-if="showFeaturesPanel" /> <features-panel v-if="showFeaturesPanel" />
</div> </div>

View file

@ -1,6 +1,8 @@
import ProgressButton from '../progress_button/progress_button.vue' import ProgressButton from '../progress_button/progress_button.vue'
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { mapState } from 'vuex'
import { import {
faEllipsisV faEllipsisV
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
@ -14,13 +16,25 @@ const AccountActions = {
'user', 'relationship' 'user', 'relationship'
], ],
data () { data () {
return { } return {
showingConfirmBlock: false
}
}, },
components: { components: {
ProgressButton, ProgressButton,
Popover Popover,
ConfirmModal
}, },
methods: { methods: {
refetchRelationship () {
return this.$store.dispatch('fetchUserRelationship', this.user.id)
},
showConfirmBlock () {
this.showingConfirmBlock = true
},
hideConfirmBlock () {
this.showingConfirmBlock = false
},
showRepeats () { showRepeats () {
this.$store.dispatch('showReblogs', this.user.id) this.$store.dispatch('showReblogs', this.user.id)
}, },
@ -28,14 +42,41 @@ const AccountActions = {
this.$store.dispatch('hideReblogs', this.user.id) this.$store.dispatch('hideReblogs', this.user.id)
}, },
blockUser () { blockUser () {
if (!this.shouldConfirmBlock) {
this.doBlockUser()
} else {
this.showConfirmBlock()
}
},
doBlockUser () {
this.$store.dispatch('blockUser', this.user.id) this.$store.dispatch('blockUser', this.user.id)
this.hideConfirmBlock()
}, },
unblockUser () { unblockUser () {
this.$store.dispatch('unblockUser', this.user.id) this.$store.dispatch('unblockUser', this.user.id)
}, },
removeUserFromFollowers () {
this.$store.dispatch('removeUserFromFollowers', this.user.id)
},
reportUser () { reportUser () {
this.$store.dispatch('openUserReportingModal', { userId: this.user.id }) this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
},
muteDomain () {
this.$store.dispatch('muteDomain', this.user.screen_name.split('@')[1])
.then(() => this.refetchRelationship())
},
unmuteDomain () {
this.$store.dispatch('unmuteDomain', this.user.screen_name.split('@')[1])
.then(() => this.refetchRelationship())
} }
},
computed: {
shouldConfirmBlock () {
return this.$store.getters.mergedConfig.modalOnBlock
},
...mapState({
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
})
} }
} }

View file

@ -28,6 +28,13 @@
class="dropdown-divider" class="dropdown-divider"
/> />
</template> </template>
<button
v-if="relationship.followed_by"
class="btn button-default btn-block dropdown-item"
@click="removeUserFromFollowers"
>
{{ $t('user_card.remove_follower') }}
</button>
<button <button
v-if="relationship.blocking" v-if="relationship.blocking"
class="btn button-default btn-block dropdown-item" class="btn button-default btn-block dropdown-item"
@ -48,6 +55,20 @@
> >
{{ $t('user_card.report') }} {{ $t('user_card.report') }}
</button> </button>
<button
v-if="relationship.domain_blocking"
class="btn button-default btn-block dropdown-item"
@click="unmuteDomain"
>
{{ $t('user_card.domain_muted') }}
</button>
<button
v-else-if="!user.is_local"
class="btn button-default btn-block dropdown-item"
@click="muteDomain"
>
{{ $t('user_card.mute_domain') }}
</button>
</div> </div>
</template> </template>
<template v-slot:trigger> <template v-slot:trigger>
@ -59,6 +80,27 @@
</button> </button>
</template> </template>
</Popover> </Popover>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmBlock"
:title="$t('user_card.block_confirm_title')"
:confirm-text="$t('user_card.block_confirm_accept_button')"
:cancel-text="$t('user_card.block_confirm_cancel_button')"
@accepted="doBlockUser"
@cancelled="hideConfirmBlock"
>
<i18n-t
keypath="user_card.block_confirm"
tag="span"
>
<template v-slot:user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
</div> </div>
</template> </template>

View file

@ -1,9 +1,9 @@
<template> <template>
<div class="panel panel-default announcements-page"> <div class="panel panel-default announcements-page">
<div class="panel-heading"> <div class="panel-heading">
<span> <div class="title">
{{ $t('announcements.page_header') }} {{ $t('announcements.page_header') }}
</span> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<section <section

View file

@ -18,6 +18,7 @@ import {
faPencilAlt, faPencilAlt,
faAlignRight faAlignRight
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import Blurhash from '../blurhash/Blurhash.vue'
library.add( library.add(
faFile, faFile,
@ -63,7 +64,8 @@ const Attachment = {
components: { components: {
Flash, Flash,
StillImage, StillImage,
VideoAttachment VideoAttachment,
Blurhash
}, },
computed: { computed: {
classNames () { classNames () {
@ -84,6 +86,9 @@ const Attachment = {
useContainFit () { useContainFit () {
return this.$store.getters.mergedConfig.useContainFit return this.$store.getters.mergedConfig.useContainFit
}, },
useBlurhash () {
return this.$store.getters.mergedConfig.useBlurhash
},
placeholderName () { placeholderName () {
if (this.attachment.description === '' || !this.attachment.description) { if (this.attachment.description === '' || !this.attachment.description) {
return this.type.toUpperCase() return this.type.toUpperCase()
@ -132,6 +137,9 @@ const Attachment = {
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
watch: { watch: {
'attachment.description' (newVal) {
this.localDescription = newVal
},
localDescription (newVal) { localDescription (newVal) {
this.onEdit(newVal) this.onEdit(newVal)
} }
@ -224,7 +232,7 @@ const Attachment = {
this.$emit('resize', newHeight) this.$emit('resize', newHeight)
}, },
postStatus (event) { postStatus (event) {
console.log(this.statusForm.postStatus(event, this.statusForm.newStatus)) this.statusForm.postStatus(event, this.statusForm.newStatus)
} }
} }
} }

View file

@ -26,6 +26,7 @@
display: flex; display: flex;
padding-top: 0.5em; padding-top: 0.5em;
z-index: 1; z-index: 1;
max-height: 50%;
p { p {
flex: 1; flex: 1;
@ -36,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: hidden; overflow: scroll;
} }
&.-static { &.-static {

View file

@ -64,7 +64,15 @@
:title="attachment.description" :title="attachment.description"
@click.prevent.stop="toggleHidden" @click.prevent.stop="toggleHidden"
> >
<Blurhash
v-if="useBlurhash && attachment.blurhash"
:height="512"
:width="1024"
:hash="attachment.blurhash"
:punch="1"
/>
<img <img
v-else
:key="nsfwImage" :key="nsfwImage"
class="nsfw" class="nsfw"
:src="nsfwImage" :src="nsfwImage"

View file

@ -0,0 +1,4 @@
export default {
computed: {
}
}

View file

@ -0,0 +1,12 @@
<template>
<div class="panel panel-default">
<div class="panel-heading">
<h4>{{ $t('registration.awaiting_email_confirmation_title') }}</h4>
</div>
<div class="panel-body">
<p>{{ $t('registration.awaiting_email_confirmation') }}</p>
</div>
</div>
</template>
<script src="./awaiting_email_confirmation.js"></script>

View file

@ -0,0 +1,66 @@
<template>
<canvas
ref="canvas"
class="blurhash"
/>
</template>
<script>
import { decode } from "blurhash";
export default {
name: 'Blurhash',
props: {
hash: {
type: String,
required: true,
},
width: {
type: Number,
required: true,
},
height: {
type: Number,
required: true,
},
punch: {
type: Number,
default: null,
},
},
data() {
return {
canvas: null,
ctx: null,
};
},
mounted() {
this.canvas = this.$refs.canvas;
this.ctx = this.canvas.getContext('2d');
this.canvas.width = 1024;
this.canvas.height = 512;
this.draw();
},
methods: {
draw() {
const pixels = decode(this.hash, this.width, this.height, this.punch);
const imageData = this.ctx.createImageData(this.width, this.height);
imageData.data.set(pixels);
this.ctx.putImageData(imageData, 0, 0);
fetch("/static/blurhash-overlay.png")
.then((response) => response.blob())
.then((blob) => {
const img = new Image();
img.src = URL.createObjectURL(blob);
img.onload = () => {
this.ctx.drawImage(img, 0, 0, this.width, this.height);
};
});
},
}
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,37 @@
import DialogModal from '../dialog_modal/dialog_modal.vue'
/**
* This component emits the following events:
* cancelled, emitted when the action should not be performed;
* accepted, emitted when the action should be performed;
*
* The caller should close this dialog after receiving any of the two events.
*/
const ConfirmModal = {
components: {
DialogModal
},
props: {
title: {
type: String
},
cancelText: {
type: String
},
confirmText: {
type: String
}
},
computed: {
},
methods: {
onCancel () {
this.$emit('cancelled')
},
onAccept () {
this.$emit('accepted')
}
}
}
export default ConfirmModal

View file

@ -0,0 +1,39 @@
<template>
<dialog-modal
v-body-scroll-lock="true"
class="confirm-modal"
:on-cancel="onCancel"
>
<template #header>
<span v-text="title" />
</template>
<slot />
<template #footer>
<button
class="btn button-default"
@click.prevent="onCancel"
v-text="cancelText"
/>
<button
class="btn button-default button-positive"
@click.prevent="onAccept"
v-text="confirmText"
/>
</template>
</dialog-modal>
</template>
<style lang="scss" scoped>
@import '../../_variables';
.confirm-modal {
.button-positive {
border: 3px solid var(--accent, $fallback--link);
border-radius: var(--btnRadius, $fallback--btnRadius);
}
}
</style>
<script src="./confirm_modal.js"></script>

View file

@ -1,6 +1,8 @@
import { reduce, filter, findIndex, clone, get } from 'lodash' import { reduce, filter, findIndex, clone, get } from 'lodash'
import Status from '../status/status.vue' import Status from '../status/status.vue'
import ThreadTree from '../thread_tree/thread_tree.vue' import ThreadTree from '../thread_tree/thread_tree.vue'
import { WSConnectionStatus } from '../../services/api/api.service.js'
import { mapGetters, mapState } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -77,6 +79,9 @@ const conversation = {
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2 const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
return maxDepth >= 1 ? maxDepth : 1 return maxDepth >= 1 ? maxDepth : 1
}, },
streamingEnabled () {
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
},
displayStyle () { displayStyle () {
return this.$store.getters.mergedConfig.conversationDisplay return this.$store.getters.mergedConfig.conversationDisplay
}, },
@ -339,7 +344,11 @@ const conversation = {
}, },
maybeHighlight () { maybeHighlight () {
return this.isExpanded ? this.highlight : null return this.isExpanded ? this.highlight : null
} },
...mapGetters(['mergedConfig']),
...mapState({
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus
})
}, },
components: { components: {
Status, Status,
@ -395,6 +404,11 @@ const conversation = {
setHighlight (id) { setHighlight (id) {
if (!id) return if (!id) return
this.highlight = id this.highlight = id
if (!this.streamingEnabled) {
this.$store.dispatch('fetchStatus', id)
}
this.$store.dispatch('fetchFavsAndRepeats', id) this.$store.dispatch('fetchFavsAndRepeats', id)
this.$store.dispatch('fetchEmojiReactionsBy', id) this.$store.dispatch('fetchEmojiReactionsBy', id)
}, },

View file

@ -1,4 +1,5 @@
import SearchBar from 'components/search_bar/search_bar.vue' import SearchBar from 'components/search_bar/search_bar.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faSignInAlt, faSignInAlt,
@ -15,7 +16,8 @@ import {
faUsers, faUsers,
faCommentMedical, faCommentMedical,
faBookmark, faBookmark,
faInfoCircle faInfoCircle,
faUserTie
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
@ -33,12 +35,14 @@ library.add(
faUsers, faUsers,
faCommentMedical, faCommentMedical,
faBookmark, faBookmark,
faInfoCircle faInfoCircle,
faUserTie
) )
export default { export default {
components: { components: {
SearchBar SearchBar,
ConfirmModal
}, },
data: () => ({ data: () => ({
searchBarHidden: true, searchBarHidden: true,
@ -48,7 +52,8 @@ export default {
window.CSS.supports('-moz-mask-size', 'contain') || window.CSS.supports('-moz-mask-size', 'contain') ||
window.CSS.supports('-ms-mask-size', 'contain') || window.CSS.supports('-ms-mask-size', 'contain') ||
window.CSS.supports('-o-mask-size', 'contain') window.CSS.supports('-o-mask-size', 'contain')
) ),
showingConfirmLogout: false
}), }),
computed: { computed: {
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask }, enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
@ -92,7 +97,13 @@ export default {
hideSitename () { return this.$store.state.instance.hideSitename }, hideSitename () { return this.$store.state.instance.hideSitename },
logoLeft () { return this.$store.state.instance.logoLeft }, logoLeft () { return this.$store.state.instance.logoLeft },
currentUser () { return this.$store.state.users.currentUser }, currentUser () { return this.$store.state.users.currentUser },
privateMode () { return this.$store.state.instance.private } privateMode () { return this.$store.state.instance.private },
shouldConfirmLogout () {
return this.$store.getters.mergedConfig.modalOnLogout
},
showBubbleTimeline () {
return this.$store.state.instance.localBubbleInstances.length > 0
}
}, },
methods: { methods: {
scrollToTop () { scrollToTop () {
@ -103,6 +114,9 @@ export default {
}, },
openSettingsModal () { openSettingsModal () {
this.$store.dispatch('openSettingsModal') this.$store.dispatch('openSettingsModal')
},
openModModal () {
this.$store.dispatch('openModModal')
} }
} }
} }

View file

@ -55,7 +55,7 @@
/> />
</router-link> </router-link>
<router-link <router-link
v-if="currentUser" v-if="currentUser && showBubbleTimeline"
:to="{ name: 'bubble-timeline' }" :to="{ name: 'bubble-timeline' }"
class="nav-icon" class="nav-icon"
> >
@ -67,6 +67,7 @@
/> />
</router-link> </router-link>
<router-link <router-link
v-if="showPublicExternalTimeline"
:to="{ name: 'public-external-timeline' }" :to="{ name: 'public-external-timeline' }"
class="nav-icon" class="nav-icon"
> >
@ -104,8 +105,8 @@
class="nav-items right" class="nav-items right"
> >
<router-link <router-link
class="nav-icon"
v-if="currentUser" v-if="currentUser"
class="nav-icon"
:to="{ name: 'interactions', params: { username: currentUser.screen_name } }" :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"
> >
<FAIcon <FAIcon
@ -151,6 +152,18 @@
:title="$t('nav.preferences')" :title="$t('nav.preferences')"
/> />
</button> </button>
<button
v-if="currentUser && currentUser.role === 'admin' || currentUser.role === 'moderator'"
class="button-unstyled nav-icon"
@click.stop="openModModal"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="user-tie"
:title="$t('nav.moderation')"
/>
</button>
<a <a
v-if="currentUser && currentUser.role === 'admin'" v-if="currentUser && currentUser.role === 'admin'"
href="/pleroma/admin/#/login-pleroma" href="/pleroma/admin/#/login-pleroma"
@ -167,6 +180,18 @@
</a> </a>
</div> </div>
</div> </div>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmLogout"
:title="$t('login.logout_confirm_title')"
:confirm-text="$t('login.logout_confirm_accept_button')"
:cancel-text="$t('login.logout_confirm_cancel_button')"
@accepted="doLogout"
@cancelled="hideConfirmLogout"
>
{{ $t('login.logout_confirm') }}
</confirm-modal>
</teleport>
</nav> </nav>
</template> </template>
<script src="./desktop_nav.js"></script> <script src="./desktop_nav.js"></script>

View file

@ -39,7 +39,7 @@
right: 0; right: 0;
top: 0; top: 0;
background: rgba(27,31,35,.5); background: rgba(27,31,35,.5);
z-index: 99; z-index: 2000;
} }
} }
@ -51,9 +51,10 @@
margin: 15vh auto; margin: 15vh auto;
position: fixed; position: fixed;
transform: translateX(-50%); transform: translateX(-50%);
z-index: 999; z-index: 2001;
cursor: default; cursor: default;
display: block; display: block;
width: max-content;
background-color: $fallback--bg; background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg); background-color: var(--bg, $fallback--bg);

View file

@ -0,0 +1,75 @@
import PostStatusForm from '../post_status_form/post_status_form.vue'
import Modal from '../modal/modal.vue'
import statusPosterService from '../../services/status_poster/status_poster.service.js'
import get from 'lodash/get'
const EditStatusModal = {
components: {
PostStatusForm,
Modal
},
data () {
return {
resettingForm: false
}
},
computed: {
isLoggedIn () {
return !!this.$store.state.users.currentUser
},
modalActivated () {
return this.$store.state.editStatus.modalActivated
},
isFormVisible () {
return this.isLoggedIn && !this.resettingForm && this.modalActivated
},
params () {
return this.$store.state.editStatus.params || {}
}
},
watch: {
params (newVal, oldVal) {
if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
this.resettingForm = true
this.$nextTick(() => {
this.resettingForm = false
})
}
},
isFormVisible (val) {
if (val) {
this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
}
}
},
methods: {
doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
const params = {
store: this.$store,
statusId: this.$store.state.editStatus.params.statusId,
status,
spoilerText,
sensitive,
poll,
media,
contentType
}
return statusPosterService.editStatus(params)
.then((data) => {
return data
})
.catch((err) => {
console.error('Error editing status', err)
return {
error: err.message
}
})
},
closeModal () {
this.$store.dispatch('closeEditStatusModal')
}
}
}
export default EditStatusModal

View file

@ -0,0 +1,48 @@
<template>
<Modal
v-if="isFormVisible"
class="edit-form-modal-view"
@backdropClicked="closeModal"
>
<div class="edit-form-modal-panel panel">
<div class="panel-heading">
{{ $t('post_status.edit_status') }}
</div>
<PostStatusForm
class="panel-body"
v-bind="params"
@posted="closeModal"
:disablePolls="true"
:disableVisibilitySelector="true"
:post-handler="doEditStatus"
/>
</div>
</Modal>
</template>
<script src="./edit_status_modal.js"></script>
<style lang="scss">
.modal-view.edit-form-modal-view {
align-items: flex-start;
}
.edit-form-modal-panel {
flex-shrink: 0;
margin-top: 25%;
margin-bottom: 2em;
width: 100%;
max-width: 700px;
@media (orientation: landscape) {
margin-top: 8%;
}
.form-bottom-left {
max-width: 6.5em;
.emoji-icon {
justify-content: right;
}
}
}
</style>

View file

@ -0,0 +1,133 @@
const EMOJI_SIZE = 32 + 8
const GROUP_TITLE_HEIGHT = 24
const BUFFER_SIZE = 3 * EMOJI_SIZE
const EmojiGrid = {
props: {
groups: {
required: true,
type: Array
}
},
data () {
return {
containerWidth: 0,
containerHeight: 0,
scrollPos: 0,
resizeObserver: null
}
},
mounted () {
const rect = this.$refs.container.getBoundingClientRect()
this.containerWidth = rect.width
this.containerHeight = rect.height
this.resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
this.containerWidth = entry.contentRect.width
this.containerHeight = entry.contentRect.height
}
})
this.resizeObserver.observe(this.$refs.container)
},
beforeUnmount () {
this.resizeObserver.disconnect()
this.resizeObserver = null
},
watch: {
groups () {
// Scroll to top when grid content changes
if (this.$refs.container) {
this.$refs.container.scrollTo(0, 0)
}
},
activeGroup (group) {
this.$emit('activeGroup', group)
}
},
methods: {
onScroll () {
this.scrollPos = this.$refs.container.scrollTop
},
onEmoji (emoji) {
this.$emit('emoji', emoji)
},
scrollToItem (itemId) {
const container = this.$refs.container
if (!container) return
for (const item of this.itemList) {
if (item.id === itemId) {
container.scrollTo(0, item.position.y)
return
}
}
}
},
computed: {
// Total height of scroller content
gridHeight () {
if (this.itemList.length === 0) return 0
const lastItem = this.itemList[this.itemList.length - 1]
return (
lastItem.position.y +
('title' in lastItem ? GROUP_TITLE_HEIGHT : EMOJI_SIZE)
)
},
activeGroup () {
const items = this.itemList
for (let i = items.length - 1; i >= 0; i--) {
const item = items[i]
if ('title' in item && item.position.y <= this.scrollPos) {
return item.id
}
}
return null
},
itemList () {
const items = []
let x = 0
let y = 0
for (const group of this.groups) {
items.push({ position: { x, y }, id: group.id, title: group.text })
if (group.text.length) {
y += GROUP_TITLE_HEIGHT
}
for (const emoji of group.emojis) {
items.push({
position: { x, y },
id: `${group.id}-${emoji.displayText}`,
emoji
})
x += EMOJI_SIZE
if (x + EMOJI_SIZE > this.containerWidth) {
y += EMOJI_SIZE
x = 0
}
}
if (x > 0) {
y += EMOJI_SIZE
x = 0
}
}
return items
},
visibleItems () {
const startPos = this.scrollPos - BUFFER_SIZE
const endPos = this.scrollPos + this.containerHeight + BUFFER_SIZE
return this.itemList.filter((i) => {
return i.position.y >= startPos && i.position.y < endPos
})
},
scrolledClass () {
if (this.scrollPos <= 5) {
return 'scrolled-top'
} else if (this.scrollPos >= this.gridHeight - this.containerHeight - 5) {
return 'scrolled-bottom'
} else {
return 'scrolled-middle'
}
}
}
}
export default EmojiGrid

View file

@ -0,0 +1,60 @@
.emoji {
&-grid {
flex: 1 1 1px;
position: relative;
overflow: auto;
user-select: none;
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
linear-gradient(to top, white, white);
transition: mask-size 150ms;
mask-size: 100% 20px, 100% 20px, auto;
// Autoprefixed seem to ignore this one, and also syntax is different
-webkit-mask-composite: xor;
mask-composite: exclude;
&.scrolled {
&-top {
mask-size: 100% 20px, 100% 0, auto;
}
&-bottom {
mask-size: 100% 0, 100% 20px, auto;
}
}
margin-left: 5px;
min-height: 200px;
}
&-group-title {
position: absolute;
font-size: 0.85em;
width: 100%;
margin: 0;
height: 24px;
display: flex;
align-items: end;
&.disabled {
display: none;
}
}
&-item {
position: absolute;
width: 32px;
height: 32px;
box-sizing: border-box;
display: flex;
font-size: 32px;
align-items: center;
justify-content: center;
margin: 4px;
cursor: pointer;
img {
object-fit: contain;
max-width: 100%;
max-height: 100%;
}
}
}

View file

@ -0,0 +1,48 @@
<template>
<div
ref="container"
class="emoji-grid"
:class="scrolledClass"
@scroll.passive="onScroll"
>
<div
:style="{
height: `${gridHeight}px`,
}"
>
<template v-for="item in visibleItems">
<h6
v-if="'title' in item && item.title.length"
:key="'title-' + item.id"
class="emoji-group-title"
:style="{
top: item.position.y + 'px',
left: item.position.x + 'px'
}"
>
{{ item.title }}
</h6>
<span
v-else-if="'emoji' in item"
:key="'emoji-' + item.id"
class="emoji-item"
:title="item.emoji.displayText"
:style="{
top: item.position.y + 'px',
left: item.position.x + 'px'
}"
@click.stop.prevent="onEmoji(item.emoji)"
>
<span v-if="!item.emoji.imageUrl">{{ item.emoji.replacement }}</span>
<img
v-else
:src="item.emoji.imageUrl"
>
</span>
</template>
</div>
</div>
</template>
<script src="./emoji_grid.js"></script>
<style lang="scss" src="./emoji_grid.scss"></style>

View file

@ -178,7 +178,7 @@ const EmojiInput = {
textAtCaret: async function (newWord) { textAtCaret: async function (newWord) {
const firstchar = newWord.charAt(0) const firstchar = newWord.charAt(0)
this.suggestions = [] this.suggestions = []
if (newWord === firstchar) return if (newWord === firstchar && firstchar !== '$') return
const matchedSuggestions = await this.suggest(newWord) const matchedSuggestions = await this.suggest(newWord)
// Async: cancel if textAtCaret has changed during wait // Async: cancel if textAtCaret has changed during wait
if (this.textAtCaret !== newWord) return if (this.textAtCaret !== newWord) return
@ -205,7 +205,6 @@ const EmojiInput = {
}, },
triggerShowPicker () { triggerShowPicker () {
this.showPicker = true this.showPicker = true
this.$refs.picker.startEmojiLoad()
this.$nextTick(() => { this.$nextTick(() => {
this.scrollIntoView() this.scrollIntoView()
this.focusPickerInput() this.focusPickerInput()
@ -223,7 +222,6 @@ const EmojiInput = {
this.showPicker = !this.showPicker this.showPicker = !this.showPicker
if (this.showPicker) { if (this.showPicker) {
this.scrollIntoView() this.scrollIntoView()
this.$refs.picker.startEmojiLoad()
this.$nextTick(this.focusPickerInput) this.$nextTick(this.focusPickerInput)
} }
}, },
@ -277,7 +275,6 @@ const EmojiInput = {
}, },
replaceText (e, suggestion) { replaceText (e, suggestion) {
const len = this.suggestions.length || 0 const len = this.suggestions.length || 0
if (this.textAtCaret.length === 1) { return }
if (len > 0 || suggestion) { if (len > 0 || suggestion) {
const chosenSuggestion = suggestion || this.suggestions[this.highlighted] const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
const replacement = chosenSuggestion.replacement const replacement = chosenSuggestion.replacement

View file

@ -18,6 +18,7 @@
<EmojiPicker <EmojiPicker
v-if="enableEmojiPicker" v-if="enableEmojiPicker"
ref="picker" ref="picker"
show-keep-open
:class="{ hide: !showPicker }" :class="{ hide: !showPicker }"
:enable-sticker-picker="enableStickerPicker" :enable-sticker-picker="enableStickerPicker"
class="emoji-picker-panel" class="emoji-picker-panel"
@ -42,7 +43,7 @@
:class="{ highlighted: index === highlighted }" :class="{ highlighted: index === highlighted }"
@click.stop.prevent="onClick($event, suggestion)" @click.stop.prevent="onClick($event, suggestion)"
> >
<span class="image"> <span v-if="!suggestion.mfm" class="image">
<img <img
v-if="suggestion.img" v-if="suggestion.img"
:src="suggestion.img" :src="suggestion.img"

View file

@ -1,3 +1,6 @@
const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow', 'rotate', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
.map(tag => ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true }))
/** /**
* suggest - generates a suggestor function to be used by emoji-input * suggest - generates a suggestor function to be used by emoji-input
* data: object providing source information for specific types of suggestions: * data: object providing source information for specific types of suggestions:
@ -21,6 +24,10 @@ export default data => {
if (firstChar === '@' && usersCurry) { if (firstChar === '@' && usersCurry) {
return usersCurry(input) return usersCurry(input)
} }
if (firstChar === '$') {
return MFM_TAGS
.filter(({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1)
}
return [] return []
} }
} }

View file

@ -1,5 +1,6 @@
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import EmojiGrid from '../emoji_grid/emoji_grid.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faBoxOpen, faBoxOpen,
@ -14,19 +15,17 @@ library.add(
faSmileBeam faSmileBeam
) )
// At widest, approximately 20 emoji are visible in a row,
// loading 3 rows, could be overkill for narrow picker
const LOAD_EMOJI_BY = 60
// When to start loading new batch emoji, in pixels
const LOAD_EMOJI_MARGIN = 64
const EmojiPicker = { const EmojiPicker = {
props: { props: {
enableStickerPicker: { enableStickerPicker: {
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false
},
showKeepOpen: {
required: false,
type: Boolean,
default: false
} }
}, },
data () { data () {
@ -34,16 +33,13 @@ const EmojiPicker = {
keyword: '', keyword: '',
activeGroup: 'standard', activeGroup: 'standard',
showingStickers: false, showingStickers: false,
groupsScrolledClass: 'scrolled-top', keepOpen: false
keepOpen: false,
customEmojiBufferSlice: LOAD_EMOJI_BY,
customEmojiTimeout: null,
customEmojiLoadAllConfirmed: false
} }
}, },
components: { components: {
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')), StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
Checkbox Checkbox,
EmojiGrid
}, },
methods: { methods: {
onStickerUploaded (e) { onStickerUploaded (e) {
@ -56,77 +52,19 @@ const EmojiPicker = {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen }) this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
}, },
onScroll (e) { onWheel (e) {
const target = (e && e.target) || this.$refs['emoji-groups'] e.preventDefault()
this.updateScrolledClass(target) this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
this.scrolledGroup(target)
this.triggerLoadMore(target)
}, },
highlight (key) { highlight (key) {
this.setShowStickers(false) this.setShowStickers(false)
this.activeGroup = key this.activeGroup = key
}, if (this.keyword.length) {
updateScrolledClass (target) { this.$refs.emojiGrid.scrollToItem(key)
if (target.scrollTop <= 5) {
this.groupsScrolledClass = 'scrolled-top'
} else if (target.scrollTop >= target.scrollTopMax - 5) {
this.groupsScrolledClass = 'scrolled-bottom'
} else {
this.groupsScrolledClass = 'scrolled-middle'
} }
}, },
triggerLoadMore (target) { onActiveGroup (group) {
const ref = this.$refs['group-end-custom'] this.activeGroup = group
if (!ref) return
const bottom = ref.offsetTop + ref.offsetHeight
const scrollerBottom = target.scrollTop + target.clientHeight
const scrollerTop = target.scrollTop
const scrollerMax = target.scrollHeight
// Loads more emoji when they come into view
const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
// Always load when at the very top in case there's no scroll space yet
const atTop = scrollerTop < 5
// Don't load when looking at unicode category or at the very bottom
const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
if (!bottomAboveViewport && (approachingBottom || atTop)) {
this.loadEmoji()
}
},
scrolledGroup (target) {
const top = target.scrollTop + 5
this.$nextTick(() => {
this.emojisView.forEach(group => {
const ref = this.$refs['group-' + group.id]
if (ref.offsetTop <= top) {
this.activeGroup = group.id
}
})
})
},
loadEmoji () {
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
if (allLoaded) {
return
}
this.customEmojiBufferSlice += LOAD_EMOJI_BY
},
startEmojiLoad (forceUpdate = false) {
if (!forceUpdate) {
this.keyword = ''
}
this.$nextTick(() => {
this.$refs['emoji-groups'].scrollTop = 0
})
const bufferSize = this.customEmojiBuffer.length
const bufferPrefilledAll = bufferSize === this.filteredEmoji.length
if (bufferPrefilledAll && !forceUpdate) {
return
}
this.customEmojiBufferSlice = LOAD_EMOJI_BY
}, },
toggleStickers () { toggleStickers () {
this.showingStickers = !this.showingStickers this.showingStickers = !this.showingStickers
@ -138,17 +76,10 @@ const EmojiPicker = {
if (this.keyword === '') return list if (this.keyword === '') return list
const regex = new RegExp(escapeRegExp(trim(this.keyword)), 'i') const regex = new RegExp(escapeRegExp(trim(this.keyword)), 'i')
return list.filter(emoji => { return list.filter(emoji => {
return regex.test(emoji.displayText) return (regex.test(emoji.displayText) || (!emoji.imageUrl && emoji.replacement === this.keyword))
}) })
} }
}, },
watch: {
keyword () {
this.customEmojiLoadAllConfirmed = false
this.onScroll()
this.startEmojiLoad(true)
}
},
computed: { computed: {
activeGroupView () { activeGroupView () {
return this.showingStickers ? '' : this.activeGroup return this.showingStickers ? '' : this.activeGroup
@ -164,9 +95,6 @@ const EmojiPicker = {
this.$store.state.instance.customEmoji || [] this.$store.state.instance.customEmoji || []
) )
}, },
customEmojiBuffer () {
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
},
emojis () { emojis () {
const standardEmojis = this.$store.state.instance.emoji || [] const standardEmojis = this.$store.state.instance.emoji || []
const customEmojis = this.sortedEmoji const customEmojis = this.sortedEmoji

View file

@ -1,5 +1,40 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
// The worst query selector ever
// selects ONLY emojis pickers in replies in notifications
// who thought this was a good idea?
.notification > .Status > .status-container > .post-status-form > form > .form-group > .emoji-input > .emoji-picker {
max-width: 100%;
left: 0;
@media (min-width: 1300px) {
left: -30px;
}
}
.Notification {
.emoji-picker {
min-width: 160%;
width: 150%;
overflow: hidden;
left: -70%;
max-width: 100%;
@media (min-width: 800px) and (max-width: 1280px) {
left: -50%;
min-width: 50%;
max-width: 130%;
}
@media (max-width: 800px) {
left: -10%;
min-width: 50%;
max-width: 130%;
}
.Status > .emoji-picker {
z-index: 1000;
}
}
}
.emoji-picker { .emoji-picker {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -50,10 +85,6 @@
flex-grow: 1; flex-grow: 1;
} }
.emoji-groups {
min-height: 200px;
}
.additional-tabs { .additional-tabs {
border-left: 1px solid; border-left: 1px solid;
border-left-color: $fallback--icon; border-left-color: $fallback--icon;
@ -132,8 +163,7 @@
} }
} }
.emoji { .emoji-search {
&-search {
padding: 5px; padding: 5px;
flex: 0 0 auto; flex: 0 0 auto;
@ -141,67 +171,4 @@
width: 100%; width: 100%;
} }
} }
&-groups {
flex: 1 1 1px;
position: relative;
overflow: auto;
user-select: none;
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
linear-gradient(to top, white, white);
transition: mask-size 150ms;
mask-size: 100% 20px, 100% 20px, auto;
// Autoprefixed seem to ignore this one, and also syntax is different
-webkit-mask-composite: xor;
mask-composite: exclude;
&.scrolled {
&-top {
mask-size: 100% 20px, 100% 0, auto;
}
&-bottom {
mask-size: 100% 0, 100% 20px, auto;
}
}
}
&-group {
display: flex;
align-items: center;
flex-wrap: wrap;
padding-left: 5px;
justify-content: left;
&-title {
font-size: 0.85em;
width: 100%;
margin: 0;
&.disabled {
display: none;
}
}
}
&-item {
width: 32px;
height: 32px;
box-sizing: border-box;
display: flex;
font-size: 32px;
align-items: center;
justify-content: center;
margin: 4px;
cursor: pointer;
img {
object-fit: contain;
max-width: 100%;
max-height: 100%;
}
}
}
} }

View file

@ -1,7 +1,11 @@
<template> <template>
<div class="emoji-picker panel panel-default panel-body"> <div class="emoji-picker panel panel-default panel-body">
<div class="heading"> <div class="heading">
<span class="emoji-tabs"> <span
ref="emoji-tabs"
class="emoji-tabs"
@wheel="onWheel"
>
<span <span
v-for="group in emojis" v-for="group in emojis"
:key="group.id" :key="group.id"
@ -47,40 +51,16 @@
@input="$event.target.composing = false" @input="$event.target.composing = false"
> >
</div> </div>
<EmojiGrid
ref="emojiGrid"
:groups="emojisView"
@emoji="onEmoji"
@active-group="onActiveGroup"
/>
<div <div
ref="emoji-groups" v-if="showKeepOpen"
class="emoji-groups" class="keep-open"
:class="groupsScrolledClass"
@scroll="onScroll"
> >
<div
v-for="group in emojisView"
:key="group.id"
class="emoji-group"
>
<h6
:ref="'group-' + group.id"
class="emoji-group-title"
>
{{ group.text }}
</h6>
<span
v-for="emoji in group.emojis"
:key="group.id + emoji.displayText"
:title="emoji.displayText"
class="emoji-item"
@click.stop.prevent="onEmoji(emoji)"
>
<span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
<img
v-else
:src="emoji.imageUrl"
>
</span>
<span :ref="'group-end-' + group.id" />
</div>
</div>
<div class="keep-open">
<Checkbox v-model="keepOpen"> <Checkbox v-model="keepOpen">
{{ $t('emoji.keep_open') }} {{ $t('emoji.keep_open') }}
</Checkbox> </Checkbox>

View file

@ -46,14 +46,6 @@ const EmojiReactions = {
reactedWith (emoji) { reactedWith (emoji) {
return this.status.emoji_reactions.find(r => r.name === emoji).me return this.status.emoji_reactions.find(r => r.name === emoji).me
}, },
isLocalReaction (emojiUrl) {
if (!emojiUrl) return true
const reacted = this.accountsForEmoji[emojiUrl]
if (reacted.length === 0) {
return true
}
return reacted[0].is_local
},
fetchEmojiReactionsByIfMissing () { fetchEmojiReactionsByIfMissing () {
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts) const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
if (hasNoAccounts) { if (hasNoAccounts) {

View file

@ -8,7 +8,6 @@
<button <button
class="emoji-reaction btn button-default" class="emoji-reaction btn button-default"
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }" :class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
:disabled="!isLocalReaction(reaction.url)"
@click="emojiOnClick(reaction.name, $event)" @click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()" @mouseenter="fetchEmojiReactionsByIfMissing()"
> >
@ -20,6 +19,7 @@
:title="reaction.name" :title="reaction.name"
class="reaction-emoji" class="reaction-emoji"
width="2.55em" width="2.55em"
height="2.55em"
> >
{{ reaction.count }} {{ reaction.count }}
</span> </span>
@ -66,6 +66,7 @@
box-sizing: border-box; box-sizing: border-box;
.reaction-emoji { .reaction-emoji {
width: 2.55em !important; width: 2.55em !important;
height: 2.55em !important;
margin-right: 0.25em; margin-right: 0.25em;
} }
&:focus { &:focus {
@ -93,7 +94,7 @@
} }
} }
.picked-reaction { .button-default.picked-reaction {
border: 1px solid var(--accent, $fallback--link); border: 1px solid var(--accent, $fallback--link);
margin-left: -1px; // offset the border, can't use inset shadows either margin-left: -1px; // offset the border, can't use inset shadows either
margin-right: calc(0.5em - 1px); margin-right: calc(0.5em - 1px);

View file

@ -1,4 +1,5 @@
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faEllipsisH, faEllipsisH,
@ -6,7 +7,9 @@ import {
faEyeSlash, faEyeSlash,
faThumbtack, faThumbtack,
faShareAlt, faShareAlt,
faExternalLinkAlt faExternalLinkAlt,
faHistory,
faFilePen
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { import {
faBookmark as faBookmarkReg, faBookmark as faBookmarkReg,
@ -21,19 +24,51 @@ library.add(
faThumbtack, faThumbtack,
faShareAlt, faShareAlt,
faExternalLinkAlt, faExternalLinkAlt,
faFlag faFlag,
faHistory,
faFilePen
) )
const ExtraButtons = { const ExtraButtons = {
props: [ 'status' ], props: ['status'],
components: { Popover }, components: {
Popover,
ConfirmModal
},
data () {
return {
expanded: false,
showingDeleteDialog: false,
showingRedraftDialog: false
}
},
methods: { methods: {
deleteStatus () { deleteStatus () {
const confirmed = window.confirm(this.$t('status.delete_confirm')) if (this.shouldConfirmDelete) {
if (confirmed) { this.showDeleteStatusConfirmDialog()
this.$store.dispatch('deleteStatus', { id: this.status.id }) } else {
this.doDeleteStatus()
} }
}, },
doDeleteStatus () {
this.$store.dispatch('deleteStatus', { id: this.status.id })
this.hideDeleteStatusConfirmDialog()
},
showDeleteStatusConfirmDialog () {
this.showingDeleteDialog = true
},
hideDeleteStatusConfirmDialog () {
this.showingDeleteDialog = false
},
translateStatus () {
if (this.noTranslationTargetSet) {
this.$store.dispatch('pushGlobalNotice', { messageKey: 'toast.no_translation_target_set', level: 'info' })
}
const translateTo = this.$store.getters.mergedConfig.translationLanguage || this.$store.state.instance.interfaceLanguage
this.$store.dispatch('translateStatus', { id: this.status.id, language: translateTo })
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
},
pinStatus () { pinStatus () {
this.$store.dispatch('pinStatus', this.status.id) this.$store.dispatch('pinStatus', this.status.id)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
@ -71,6 +106,54 @@ const ExtraButtons = {
}, },
reportStatus () { reportStatus () {
this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] }) this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
},
editStatus () {
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
.then(data => this.$store.dispatch('openEditStatusModal', {
statusId: this.status.id,
subject: data.spoiler_text,
statusText: data.text,
statusIsSensitive: this.status.nsfw,
statusPoll: this.status.poll,
statusFiles: [...this.status.attachments],
visibility: this.status.visibility,
statusContentType: data.content_type
}))
},
showStatusHistory () {
const originalStatus = { ...this.status }
const stripFieldsList = ['attachments', 'created_at', 'emojis', 'text', 'raw_html', 'nsfw', 'poll', 'summary', 'summary_raw_html']
stripFieldsList.forEach(p => delete originalStatus[p])
this.$store.dispatch('openStatusHistoryModal', originalStatus)
},
redraftStatus () {
if (this.shouldConfirmDelete) {
this.showRedraftStatusConfirmDialog()
} else {
this.doRedraftStatus()
}
},
doRedraftStatus () {
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
.then(data => this.$store.dispatch('openPostStatusModal', {
isRedraft: true,
statusId: this.status.id,
subject: data.spoiler_text,
statusText: data.text,
statusIsSensitive: this.status.nsfw,
statusPoll: this.status.poll,
statusFiles: [...this.status.attachments],
statusScope: this.status.visibility,
statusLanguage: this.status.language,
statusContentType: data.content_type
}))
this.doDeleteStatus()
},
showRedraftStatusConfirmDialog () {
this.showingRedraftDialog = true
},
hideRedraftStatusConfirmDialog () {
this.showingRedraftDialog = false
} }
}, },
computed: { computed: {
@ -89,9 +172,26 @@ const ExtraButtons = {
canMute () { canMute () {
return !!this.currentUser return !!this.currentUser
}, },
canTranslate () {
return this.$store.state.instance.translationEnabled === true
},
noTranslationTargetSet () {
return this.$store.getters.mergedConfig.translationLanguage === undefined
},
statusLink () { statusLink () {
if (this.status.is_local) {
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}` return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
} else {
return this.status.external_url
} }
},
shouldConfirmDelete () {
return this.$store.getters.mergedConfig.modalOnDelete
},
isEdited () {
return this.status.edited_at !== null
},
editingAvailable () { return this.$store.state.instance.editingAvailable }
} }
} }

View file

@ -73,6 +73,39 @@
icon="bookmark" icon="bookmark"
/><span>{{ $t("status.unbookmark") }}</span> /><span>{{ $t("status.unbookmark") }}</span>
</button> </button>
<button
v-if="ownStatus && editingAvailable"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="editStatus"
@click="close"
>
<FAIcon
fixed-width
icon="pen"
/><span>{{ $t("status.edit") }}</span>
</button>
<button
v-if="isEdited && editingAvailable"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="showStatusHistory"
@click="close"
>
<FAIcon
fixed-width
icon="history"
/><span>{{ $t("status.edit_history") }}</span>
</button>
<button
v-if="ownStatus"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="redraftStatus"
@click="close"
>
<FAIcon
fixed-width
icon="file-pen"
/><span>{{ $t("status.redraft") }}</span>
</button>
<button <button
v-if="canDelete" v-if="canDelete"
class="button-default dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@ -116,6 +149,27 @@
:icon="['far', 'flag']" :icon="['far', 'flag']"
/><span>{{ $t("user_card.report") }}</span> /><span>{{ $t("user_card.report") }}</span>
</button> </button>
<button
v-if="canTranslate"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="translateStatus"
@click="close"
>
<FAIcon
fixed-width
icon="globe"
/><span>{{ $t("status.translate") }}</span>
<template v-if="noTranslationTargetSet">
<span class="dropdown-item-icon__badge warning">
<FAIcon
fixed-width
icon="exclamation-triangle"
name="test"
/>
</span>
</template>
</button>
</div> </div>
</template> </template>
<template v-slot:trigger> <template v-slot:trigger>
@ -125,6 +179,28 @@
icon="ellipsis-h" icon="ellipsis-h"
/> />
</button> </button>
<teleport to="#modal">
<ConfirmModal
v-if="showingDeleteDialog"
:title="$t('status.delete_confirm_title')"
:cancel-text="$t('status.delete_confirm_cancel_button')"
:confirm-text="$t('status.delete_confirm_accept_button')"
@cancelled="hideDeleteStatusConfirmDialog"
@accepted="doDeleteStatus"
>
{{ $t('status.delete_confirm') }}
</ConfirmModal>
<ConfirmModal
v-if="showingRedraftDialog"
:title="$t('status.redraft_confirm_title')"
:cancel-text="$t('status.redraft_confirm_cancel_button')"
:confirm-text="$t('status.redraft_confirm_accept_button')"
@cancelled="hideRedraftStatusConfirmDialog"
@accepted="doRedraftStatus"
>
{{ $t('status.redraft_confirm') }}
</ConfirmModal>
</teleport>
</template> </template>
</Popover> </Popover>
</template> </template>

View file

@ -31,7 +31,10 @@ const FavoriteButton = {
} }
}, },
computed: { computed: {
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig']),
remoteInteractionLink () {
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
}
} }
} }

View file

@ -13,13 +13,19 @@
:spin="animated" :spin="animated"
/> />
</button> </button>
<span v-else> <a
v-else
class="button-unstyled interactive"
target="_blank"
role="button"
:href="remoteInteractionLink"
>
<FAIcon <FAIcon
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
:title="$t('tool_tip.favorite')" :title="$t('tool_tip.favorite')"
:icon="['far', 'star']" :icon="['far', 'star']"
/> />
</span> </a>
<span <span
v-if="!mergedConfig.hidePostStats && status.fave_num > 0" v-if="!mergedConfig.hidePostStats && status.fave_num > 0"
class="action-counter" class="action-counter"

View file

@ -4,7 +4,6 @@ const FeaturesPanel = {
computed: { computed: {
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled }, whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable }, mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode },
textlimit: function () { return this.$store.state.instance.textlimit }, textlimit: function () { return this.$store.state.instance.textlimit },
uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) } uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) }
} }

View file

@ -1,12 +1,20 @@
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
export default { export default {
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'], props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
components: {
ConfirmModal
},
data () { data () {
return { return {
inProgress: false inProgress: false,
showingConfirmUnfollow: false
} }
}, },
computed: { computed: {
shouldConfirmUnfollow () {
return this.$store.getters.mergedConfig.modalOnUnfollow
},
isPressed () { isPressed () {
return this.inProgress || this.relationship.following return this.inProgress || this.relationship.following
}, },
@ -35,6 +43,12 @@ export default {
} }
}, },
methods: { methods: {
showConfirmUnfollow () {
this.showingConfirmUnfollow = true
},
hideConfirmUnfollow () {
this.showingConfirmUnfollow = false
},
onClick () { onClick () {
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow() this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow()
}, },
@ -45,12 +59,21 @@ export default {
}) })
}, },
unfollow () { unfollow () {
if (this.shouldConfirmUnfollow) {
this.showConfirmUnfollow()
} else {
this.doUnfollow()
}
},
doUnfollow () {
const store = this.$store const store = this.$store
this.inProgress = true this.inProgress = true
requestUnfollow(this.relationship.id, store).then(() => { requestUnfollow(this.relationship.id, store).then(() => {
this.inProgress = false this.inProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id }) store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id })
}) })
this.hideConfirmUnfollow()
} }
} }
} }

View file

@ -7,6 +7,27 @@
@click="onClick" @click="onClick"
> >
{{ label }} {{ label }}
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmUnfollow"
:title="$t('user_card.unfollow_confirm_title')"
:confirm-text="$t('user_card.unfollow_confirm_accept_button')"
:cancel-text="$t('user_card.unfollow_confirm_cancel_button')"
@accepted="doUnfollow"
@cancelled="hideConfirmUnfollow"
>
<i18n-t
keypath="user_card.unfollow_confirm"
tag="span"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
</button> </button>
</template> </template>

View file

@ -1,6 +1,7 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue' import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue' import RemoteFollow from '../remote_follow/remote_follow.vue'
import FollowButton from '../follow_button/follow_button.vue' import FollowButton from '../follow_button/follow_button.vue'
import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue'
const FollowCard = { const FollowCard = {
props: [ props: [
@ -10,7 +11,8 @@ const FollowCard = {
components: { components: {
BasicUserCard, BasicUserCard,
RemoteFollow, RemoteFollow,
FollowButton FollowButton,
RemoveFollowerButton
}, },
computed: { computed: {
isMe () { isMe () {

View file

@ -22,6 +22,11 @@
class="follow-card-follow-button" class="follow-card-follow-button"
:user="user" :user="user"
/> />
<RemoveFollowerButton
v-if="noFollowsYou && relationship.followed_by"
:relationship="relationship"
class="follow-card-button"
/>
</template> </template>
</div> </div>
</basic-user-card> </basic-user-card>
@ -40,6 +45,12 @@
line-height: 1.5em; line-height: 1.5em;
} }
&-button {
margin-top: 0.5em;
padding: 0 1.5em;
margin-left: 1em;
}
&-follow-button { &-follow-button {
margin-top: 0.5em; margin-top: 0.5em;
margin-left: auto; margin-left: auto;

View file

@ -1,10 +1,18 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue' import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js' import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js'
const FollowRequestCard = { const FollowRequestCard = {
props: ['user'], props: ['user'],
components: { components: {
BasicUserCard BasicUserCard,
ConfirmModal
},
data () {
return {
showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false
}
}, },
methods: { methods: {
findFollowRequestNotificationId () { findFollowRequestNotificationId () {
@ -13,9 +21,29 @@ const FollowRequestCard = {
) )
return notif && notif.id return notif && notif.id
}, },
showApproveConfirmDialog () {
this.showingApproveConfirmDialog = true
},
hideApproveConfirmDialog () {
this.showingApproveConfirmDialog = false
},
showDenyConfirmDialog () {
this.showingDenyConfirmDialog = true
},
hideDenyConfirmDialog () {
this.showingDenyConfirmDialog = false
},
approveUser () { approveUser () {
if (this.shouldConfirmApprove) {
this.showApproveConfirmDialog()
} else {
this.doApprove()
}
},
doApprove () {
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
this.$store.dispatch('decrementFollowRequestsCount')
const notifId = this.findFollowRequestNotificationId() const notifId = this.findFollowRequestNotificationId()
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId }) this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
@ -25,14 +53,40 @@ const FollowRequestCard = {
notification.type = 'follow' notification.type = 'follow'
} }
}) })
this.hideApproveConfirmDialog()
}, },
denyUser () { denyUser () {
if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog()
} else {
this.doDeny()
}
},
doDeny () {
const notifId = this.findFollowRequestNotificationId() const notifId = this.findFollowRequestNotificationId()
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
.then(() => { .then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: notifId }) this.$store.dispatch('dismissNotificationLocal', { id: notifId })
this.$store.dispatch('decrementFollowRequestsCount')
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
}) })
this.hideDenyConfirmDialog()
}
},
computed: {
mergedConfig () {
return this.$store.getters.mergedConfig
},
shouldConfirmApprove () {
return this.mergedConfig.modalOnApproveFollow
},
shouldConfirmDeny () {
return this.mergedConfig.modalOnDenyFollow
},
show () {
const notifId = this.$store.state.api.followRequests.find(req => req.id === this.user.id)
return notifId !== undefined
} }
} }
} }

View file

@ -1,5 +1,5 @@
<template> <template>
<basic-user-card :user="user"> <basic-user-card :user="user" v-if="show">
<div class="follow-request-card-content-container"> <div class="follow-request-card-content-container">
<button <button
class="btn button-default" class="btn button-default"
@ -14,6 +14,28 @@
{{ $t('user_card.deny') }} {{ $t('user_card.deny') }}
</button> </button>
</div> </div>
<teleport to="#modal">
<confirm-modal
v-if="showingApproveConfirmDialog"
:title="$t('user_card.approve_confirm_title')"
:confirm-text="$t('user_card.approve_confirm_accept_button')"
:cancel-text="$t('user_card.approve_confirm_cancel_button')"
@accepted="doApprove"
@cancelled="hideApproveConfirmDialog"
>
{{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
<confirm-modal
v-if="showingDenyConfirmDialog"
:title="$t('user_card.deny_confirm_title')"
:confirm-text="$t('user_card.deny_confirm_accept_button')"
:cancel-text="$t('user_card.deny_confirm_cancel_button')"
@accepted="doDeny"
@cancelled="hideDenyConfirmDialog"
>
{{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
</teleport>
</basic-user-card> </basic-user-card>
</template> </template>

View file

@ -1,10 +1,26 @@
import FollowRequestCard from '../follow_request_card/follow_request_card.vue' import FollowRequestCard from '../follow_request_card/follow_request_card.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more'
import List from '../list/list.vue'
import get from 'lodash/get'
const FollowRequestList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchFollowRequests'),
select: (props, $store) => get($store.state.api, 'followRequests', []).map(req => $store.getters.findUser(req.id)),
destroy: (props, $store) => $store.dispatch('clearFollowRequests'),
childPropName: 'items',
additionalPropNames: ['userId']
})(List);
const FollowRequests = { const FollowRequests = {
components: { components: {
FollowRequestCard FollowRequestCard,
FollowRequestList
}, },
computed: { computed: {
userId () {
return this.$store.state.users.currentUser.id
},
requests () { requests () {
return this.$store.state.api.followRequests return this.$store.state.api.followRequests
} }

View file

@ -6,12 +6,11 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<FollowRequestCard <FollowRequestList :user-id="userId">
v-for="request in requests" <template #item="{item}">
:key="request.id" <FollowRequestCard :user="item" />
:user="request" </template>
class="list-item" </FollowRequestList>
/>
</div> </div>
</div> </div>
</template> </template>

View file

@ -0,0 +1,77 @@
<template>
<div class="followed-tag-card">
<span>
<router-link :to="{ name: 'tag-timeline', params: {tag: tag.name}}">
<span class="tag-link">#{{ tag.name }}</span>
</router-link>
<span class="unfollow-tag">
<button
v-if="isFollowing"
class="button-default unfollow-tag-button"
:title="$t('user_card.unfollow_tag')"
@click="unfollowTag(tag.name)"
>
{{ $t('user_card.unfollow_tag') }}
</button>
<button
v-else
class="button-default follow-tag-button"
:title="$t('user_card.follow_tag')"
@click="followTag(tag.name)"
>
{{ $t('user_card.follow_tag') }}
</button>
</span>
</span>
</div>
</template>
<script>
export default {
name: 'FollowedTagCard',
props: {
tag: {
type: Object,
required: true
},
},
// this is a hack to update the state of the button
// for some reason, List does not update on changes to the tag object
data: () => ({
isFollowing: true
}),
mounted () {
this.isFollowing = this.tag.following
},
methods: {
unfollowTag (tag) {
this.$store.dispatch('unfollowTag', tag)
this.isFollowing = false
},
followTag (tag) {
this.$store.dispatch('followTag', tag)
this.isFollowing = true
}
}
}
</script>
<style scoped>
.followed-tag-card {
margin-left: 1rem;
margin-top: 1rem;
margin-bottom: 1rem;
}
.unfollow-tag {
position: absolute;
right: 1rem;
}
.tag-link {
font-size: large;
}
.unfollow-tag-button, .follow-tag-button {
font-size: medium;
}
</style>

View file

@ -1,5 +1,10 @@
<template> <template>
<div> <div>
<FAIcon
v-if="globeIcon"
icon="globe"
/>
{{ ' ' }}
<label for="interface-language-switcher"> <label for="interface-language-switcher">
{{ promptText }} {{ promptText }}
</label> </label>
@ -39,6 +44,10 @@ export default {
setLanguage: { setLanguage: {
type: Function, type: Function,
required: true required: true
},
globeIcon: {
type: Boolean,
default: true
} }
}, },
computed: { computed: {

View file

@ -1,7 +1,9 @@
import SideDrawer from '../side_drawer/side_drawer.vue' import SideDrawer from '../side_drawer/side_drawer.vue'
import Notifications from '../notifications/notifications.vue' import Notifications from '../notifications/notifications.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
import GestureService from '../../services/gesture_service/gesture_service' import GestureService from '../../services/gesture_service/gesture_service'
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faTimes, faTimes,
@ -18,11 +20,13 @@ library.add(
const MobileNav = { const MobileNav = {
components: { components: {
SideDrawer, SideDrawer,
Notifications Notifications,
ConfirmModal
}, },
data: () => ({ data: () => ({
notificationsCloseGesture: undefined, notificationsCloseGesture: undefined,
notificationsOpen: false notificationsOpen: false,
showingConfirmLogout: false
}), }),
created () { created () {
this.notificationsCloseGesture = GestureService.swipeGesture( this.notificationsCloseGesture = GestureService.swipeGesture(
@ -47,7 +51,11 @@ const MobileNav = {
hideSiteName () { hideSiteName () {
return this.mergedConfig.hideSiteName return this.mergedConfig.hideSiteName
}, },
sitename () { return this.$store.state.instance.name } sitename () { return this.$store.state.instance.name },
shouldConfirmLogout () {
return this.$store.getters.mergedConfig.modalOnLogout
},
...mapGetters(['unreadChatCount'])
}, },
methods: { methods: {
toggleMobileSidebar () { toggleMobileSidebar () {
@ -73,9 +81,23 @@ const MobileNav = {
scrollToTop () { scrollToTop () {
window.scrollTo(0, 0) window.scrollTo(0, 0)
}, },
showConfirmLogout () {
this.showingConfirmLogout = true
},
hideConfirmLogout () {
this.showingConfirmLogout = false
},
logout () { logout () {
if (!this.shouldConfirmLogout) {
this.doLogout()
} else {
this.showConfirmLogout()
}
},
doLogout () {
this.$router.replace('/main/public') this.$router.replace('/main/public')
this.$store.dispatch('logout') this.$store.dispatch('logout')
this.hideConfirmLogout()
}, },
markNotificationsAsSeen () { markNotificationsAsSeen () {
// this.$refs.notifications.markAsSeen() // this.$refs.notifications.markAsSeen()

View file

@ -76,6 +76,18 @@
ref="sideDrawer" ref="sideDrawer"
:logout="logout" :logout="logout"
/> />
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmLogout"
:title="$t('login.logout_confirm_title')"
:confirm-text="$t('login.logout_confirm_accept_button')"
:cancel-text="$t('login.logout_confirm_cancel_button')"
@accepted="doLogout"
@cancelled="hideConfirmLogout"
>
{{ $t('login.logout_confirm') }}
</confirm-modal>
</teleport>
</div> </div>
</template> </template>
@ -206,6 +218,14 @@
} }
} }
} }
.confirm-modal.dark-overlay {
&::before {
z-index: 3000;
}
.dialog-modal.panel {
z-index: 3001;
}
}
} }
</style> </style>

View file

@ -0,0 +1,58 @@
import Modal from 'src/components/modal/modal.vue'
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faTimes,
faChevronDown
} from '@fortawesome/free-solid-svg-icons'
import {
faWindowMinimize
} from '@fortawesome/free-regular-svg-icons'
library.add(
faTimes,
faWindowMinimize,
faChevronDown
)
const ModModal = {
components: {
Modal,
ModModalContent: getResettableAsyncComponent(
() => import('./mod_modal_content.vue'),
{
loadingComponent: PanelLoading,
errorComponent: AsyncComponentError,
delay: 0
}
)
},
methods: {
closeModal () {
this.$store.dispatch('closeModModal')
},
peekModal () {
this.$store.dispatch('togglePeekModModal')
}
},
computed: {
moderator () {
return this.$store.state.users.currentUser &&
(this.$store.state.users.currentUser.role === 'admin' ||
this.$store.state.users.currentUser.role === 'moderator')
},
modalActivated () {
return this.$store.state.interface.modModalState !== 'hidden'
},
modalOpenedOnce () {
return this.$store.state.interface.modModalLoaded
},
modalPeeked () {
return this.$store.state.interface.modModalState === 'minimized'
}
}
}
export default ModModal

View file

@ -0,0 +1,44 @@
@import 'src/_variables.scss';
.mod-modal {
overflow: hidden;
&.peek {
.mod-modal-panel {
/* Explanation:
* Modal is positioned vertically centered.
* 100vh - 100% = Distance between modal's top+bottom boundaries and screen
* (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen
* + 100% - we move modal completely off-screen, it's top boundary touches
* bottom of the screen
* - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible
*/
transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));
@media all and (max-width: 800px) {
/* For mobile, the modal takes 100% of the available screen.
This ensures the minimized modal is always 50px above the browser bottom bar regardless of whether or not it is visible.
*/
transform: translateY(calc(100% - 50px));
}
}
}
.mod-modal-panel {
overflow: hidden;
transition: transform;
transition-timing-function: ease-in-out;
transition-duration: 300ms;
width: 1000px;
max-width: 90vw;
height: 90vh;
@media all and (max-width: 800px) {
max-width: 100vw;
height: 100%;
}
.panel-body {
height: inherit;
}
}
}

View file

@ -0,0 +1,43 @@
<template>
<Modal
v-if="moderator"
:is-open="modalActivated"
class="mod-modal"
:class="{ peek: modalPeeked }"
:no-background="modalPeeked"
>
<div class="mod-modal-panel panel">
<div class="panel-heading">
<span class="title">
{{ $t('moderation.moderation') }}
</span>
<button
class="btn button-default"
:title="$t('general.peek')"
@click="peekModal"
>
<FAIcon
:icon="['far', 'window-minimize']"
fixed-width
/>
</button>
<button
class="btn button-default"
:title="$t('general.close')"
@click="closeModal"
>
<FAIcon
icon="times"
fixed-width
/>
</button>
</div>
<div class="panel-body">
<ModModalContent v-if="modalOpenedOnce" />
</div>
</div>
</Modal>
</template>
<script src="./mod_modal.js"></script>
<style src="./mod_modal.scss" lang="scss"></style>

View file

@ -0,0 +1,63 @@
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import ReportsTab from './tabs/reports_tab/reports_tab.vue'
// import StatusesTab from './tabs/statuses_tab.vue'
// import UsersTab from './tabs/users_tab.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faFlag,
faMessage,
faUsers
} from '@fortawesome/free-solid-svg-icons'
library.add(
faFlag,
faMessage,
faUsers
)
const ModModalContent = {
components: {
TabSwitcher,
ReportsTab
// StatusesTab,
// UsersTab
},
computed: {
open () {
return this.$store.state.interface.modModalState !== 'hidden'
},
bodyLock () {
return this.$store.state.interface.modModalState === 'visible'
}
},
methods: {
onOpen () {
const targetTab = this.$store.state.interface.modModalTargetTab
// We're being told to open in specific tab
if (targetTab) {
const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => {
return elm.props && elm.props['data-tab-name'] === targetTab
})
if (tabIndex >= 0) {
this.$refs.tabSwitcher.setTab(tabIndex)
}
}
// Clear the state of target tab, so that next time moderation is opened
// it doesn't force it.
this.$store.dispatch('clearModModalTargetTab')
}
},
mounted () {
this.onOpen()
},
watch: {
open: function (value) {
if (value) this.onOpen()
}
}
}
export default ModModalContent

View file

@ -0,0 +1,21 @@
@import 'src/_variables.scss';
.mod_tab-switcher {
height: 100%;
.content {
margin: 1em 1em 1.4em;
> div {
margin-bottom: .5em;
&:last-child {
margin-bottom: 0;
}
}
textarea {
width: 100%;
max-width: 100%;
height: 100px;
}
}
}

View file

@ -0,0 +1,20 @@
<template>
<tab-switcher
ref="tabSwitcher"
class="mod_tab-switcher"
:side-tab-bar="true"
:scrollable-tabs="true"
:body-scroll-lock="bodyLock"
>
<div
:label="$t('moderation.reports.reports')"
icon="flag"
data-tab-name="reports"
>
<ReportsTab />
</div>
</tab-switcher>
</template>
<script src="./mod_modal_content.js"></script>
<style src="./mod_modal_content.scss" lang="scss"></style>

View file

@ -0,0 +1,124 @@
import Popover from 'src/components/popover/popover.vue'
import Status from 'src/components/status/status.vue'
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
import ReportNote from './report_note.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faChevronDown,
faChevronUp
} from '@fortawesome/free-solid-svg-icons'
library.add(
faChevronDown,
faChevronUp
)
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
const STRIP_MEDIA = 'mrf_tag:media-strip'
const FORCE_UNLISTED = 'mrf_tag:force-unlisted'
const SANDBOX = 'mrf_tag:sandbox'
const ReportCard = {
data () {
return {
hidden: true,
statusesHidden: true,
notesHidden: true,
note: null,
tags: {
FORCE_NSFW,
STRIP_MEDIA,
FORCE_UNLISTED,
SANDBOX
}
}
},
props: [
'account',
'actor',
'content',
'id',
'notes',
'state',
'statuses'
],
components: {
ReportNote,
Popover,
Status,
UserAvatar
},
created () {
this.$store.dispatch('fetchUser', this.account.id)
},
computed: {
isOpen () {
return this.state === 'open'
},
tagPolicyEnabled () {
return this.$store.state.instance.federationPolicy.mrf_policies.includes('TagPolicy')
},
user () {
return this.$store.getters.findUser(this.account.id)
}
},
methods: {
toggleHidden () {
this.hidden = !this.hidden
},
decode (content) {
content = content.replaceAll('<br/>', '\n')
const textarea = document.createElement('textarea')
textarea.innerHTML = content
return textarea.value
},
updateReportState (state) {
this.$store.dispatch('updateReportStates', { reports: [{ id: this.id, state }] })
},
toggleNotes () {
this.notesHidden = !this.notesHidden
},
addNoteToReport () {
if (this.note.length > 0) {
this.$store.dispatch('addNoteToReport', { id: this.id, note: this.note })
this.note = null
}
},
toggleStatuses () {
this.statusesHidden = !this.statusesHidden
},
hasTag (tag) {
return this.user.tags.includes(tag)
},
toggleTag (tag) {
if (this.hasTag(tag)) {
this.$store.state.api.backendInteractor.untagUser({ user: this.user, tag }).then(response => {
if (!response.ok) { return }
this.$store.commit('untagUser', { user: this.user, tag })
})
} else {
this.$store.state.api.backendInteractor.tagUser({ user: this.user, tag }).then(response => {
if (!response.ok) { return }
this.$store.commit('tagUser', { user: this.user, tag })
})
}
},
toggleActivationStatus () {
this.$store.dispatch('toggleActivationStatus', { user: this.user })
},
deleteUser () {
this.$store.state.backendInteractor.deleteUser({ user: this.user })
.then(e => {
this.$store.dispatch('markStatusesAsDeleted', status => this.user.id === status.user.id)
const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'
const isTargetUser = this.$route.params.name === this.user.name || this.$route.params.id === this.user.id
if (isProfile && isTargetUser) {
window.history.back()
}
})
}
}
}
export default ReportCard

View file

@ -0,0 +1,202 @@
<template>
<div class="report-card panel">
<div
class="panel-heading"
@click="toggleHidden"
>
<h4>{{ $t('moderation.reports.report') + ' ' + this.account.screen_name }}</h4>
<button
v-if="isOpen"
class="button-default"
@click.stop="updateReportState('closed')"
>
{{ $t('moderation.reports.close') }}
</button>
<button
v-if="isOpen"
class="button-default"
@click.stop="updateReportState('resolved')"
>
{{ $t('moderation.reports.resolve') }}
</button>
<button
v-else
class="button-default"
@click.stop="updateReportState('open')"
>
{{ $t('moderation.reports.reopen') }}
</button>
</div>
<div
v-if="!hidden"
class="panel-body report-body"
>
<div class="report-content">
<div v-if="content">
{{ decode(content) }}
</div>
<i v-else class="faint">
{{ $t('moderation.reports.no_content') }}
</i>
<div class="report-author">
<UserAvatar
class="small-avatar"
:user="actor"
/>
{{ this.actor.screen_name }}
</div>
</div>
<div
class="dropdown"
v-if="!hidden && this.statuses.length > 0"
>
<button
class="button button-unstyled dropdown-header"
@click="toggleStatuses"
>
{{ $tc('moderation.reports.statuses', statuses.length - 1, { count: statuses.length }) }}
<FAIcon
class="timelines-chevron"
fixed-width
:icon="statusesHidden ? 'chevron-down' : 'chevron-up'"
/>
</button>
<div v-if="!statusesHidden">
<Status
v-for="status in statuses"
:key="status.id"
:collapsable="false"
:expandable="false"
:compact="false"
:statusoid="status"
:no-heading="false"
/>
</div>
</div>
<div
class="dropdown"
v-if="!hidden && this.notes.length > 0"
>
<button
class="button button-unstyled dropdown-header"
@click="toggleNotes"
>
{{ $tc('moderation.reports.notes', notes.length - 1, { count: notes.length }) }}
<FAIcon
class="timelines-chevron"
fixed-width
:icon="notesHidden ? 'chevron-down' : 'chevron-up'"
/>
</button>
<div v-if="!notesHidden">
<ReportNote
v-for="note in notes"
:key="note.id"
:report_id="id"
v-bind="note"
/>
</div>
</div>
<div class="report-add-note">
<textarea
rows="1"
cols="1"
v-model.trim="note"
:placeholder="$t('moderation.reports.note_placeholder')"
/>
<button
class="btn button-default"
@click.stop="addNoteToReport"
>
{{ $t('moderation.reports.add_note') }}
</button>
</div>
</div>
<div
v-if="!hidden"
class="panel-footer"
>
<button
class="btn button-default"
@click.stop="toggleActivationStatus"
>
{{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
</button>
<button
class="btn button-default"
@click.stop="deleteUser"
>
{{ $t('user_card.admin_menu.delete_account') }}
</button>
<Popover
trigger="click"
placement="top"
:offset="{ y: 5 }"
remove-padding
>
<template v-slot:trigger>
<button
class="btn button-default"
:disabled="!tagPolicyEnabled"
:title="tagPolicyEnabled ? '' : $t('moderation.reports.account.tag_policy_notice')"
>
<span>{{ $t("moderation.reports.tags") }}</span>
{{ ' ' }}
<FAIcon
icon="chevron-down"
/>
</button>
</template>
<template v-slot:content="{close}">
<div
class="dropdown-menu"
:disabled="!tagPolicyEnabled"
>
<button
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="toggleTag(tags.FORCE_NSFW)"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"
/>
{{ $t('user_card.admin_menu.force_nsfw') }}
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="toggleTag(tags.STRIP_MEDIA)"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"
/>
{{ $t('user_card.admin_menu.strip_media') }}
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="toggleTag(tags.FORCE_UNLISTED)"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"
/>
{{ $t('user_card.admin_menu.force_unlisted') }}
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="toggleTag(tags.SANDBOX)"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"
/>
{{ $t('user_card.admin_menu.sandbox') }}
</button>
</div>
</template>
</popover>
</div>
</div>
</template>
<script src="./report_card.js"></script>

View file

@ -0,0 +1,37 @@
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
import Timeago from 'src/components/timeago/timeago.vue'
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
const ReportNote = {
data () {
return {
showingDeleteDialog: false
}
},
props: [
'content',
'created_at',
'user',
'report_id',
'id'
],
components: {
ConfirmModal,
Timeago,
UserAvatar
},
methods: {
deleteNoteFromReport () {
this.$store.dispatch('deleteNoteFromReport', { id: this.report_id, note: this.id })
this.showingDeleteDialog = false
},
showDeleteDialog () {
this.showingDeleteDialog = true
},
hideDeleteDialog () {
this.showingDeleteDialog = false
}
}
}
export default ReportNote

View file

@ -0,0 +1,43 @@
<template>
<div class="report-note">
<div class="note-header">
<div class="note-author">
<UserAvatar
class="small-avatar"
:user="user"
/>
{{ this.user.screen_name }}
</div>
<div class="header-right">
<Timeago
class="faint"
:time="created_at"
:auto-update="60"
:long-format="true"
:with-direction="true"
/>
<button
class="btn button-default"
@click.stop="showDeleteDialog"
>
{{ $t('moderation.reports.delete_note') }}
</button>
</div>
</div>
<div class="note-content">
{{ content }}
</div>
<confirm-modal
v-if="showingDeleteDialog"
:title="$t('moderation.reports.delete_note_title')"
:confirm-text="$t('moderation.reports.delete_note_accept')"
:cancel-text="$t('moderation.reports.delete_note_cancel')"
@accepted="deleteNoteFromReport"
@cancelled="hideDeleteDialog"
>
{{ $t('moderation.reports.delete_note_confirm') }}
</confirm-modal>
</div>
</template>
<script src="./report_note.js"></script>

View file

@ -0,0 +1,26 @@
import { filter } from 'lodash'
import ReportCard from './report_card.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
const ReportsTab = {
data () {
return {
showClosed: false
}
},
components: {
Checkbox,
ReportCard
},
computed: {
reports () {
return this.$store.state.reports.reports
},
openReports () {
return filter(this.reports, { state: 'open' })
}
}
}
export default ReportsTab

View file

@ -0,0 +1,83 @@
@import '../../../../_variables.scss';
.report-card {
.report-body {
& > * {
padding: 1em;
}
& > :not(:last-child) {
border-bottom: 1px solid;
border-bottom-color: var(--border, #222);
}
.report-content {
white-space: pre-wrap;
}
.report-author {
padding-top: 0.5em;
}
.small-avatar {
height: 25px;
width: 25px;
padding-right: 0.4em;
vertical-align: middle;
}
.dropdown {
display: flex;
flex-direction: column;
padding: 0;
.dropdown-header {
padding: 1em;
color: var(--link, $fallback--link);
&:hover {
background-color: var(--selectedMenu, $fallback--lightBg);
color: var(--selectedMenuText, $fallback--link);
}
}
}
.report-note {
padding: 1em;
.note-header {
display: flex;
justify-content: space-between;
padding-bottom: 0.5em;
}
button {
margin-left: 0.5em;
}
}
.report-add-note {
textarea {
resize: none;
}
button {
min-height: 2em;
min-width: 10em;
padding: 0 2em;
margin-top: 0.5em;
}
}
}
.panel-footer {
display: flex;
& > * {
margin-right: 0.5em;
}
}
}
.reports-header {
display: flex;
align-items: center;
justify-content: space-between;
}

View file

@ -0,0 +1,25 @@
<template>
<div :label="$t('moderation.reports.reports')">
<div class="content">
<div class="reports-header">
<h2>{{ $t('moderation.reports.reports') }}</h2>
<Checkbox v-model="showClosed">
{{ $t('moderation.reports.show_closed') }}
</Checkbox>
</div>
<div class="reports">
<div v-if="(openReports.length === 0 && !showClosed) || reports.length === 0">
<p>{{ $t('moderation.reports.no_reports') }}</p>
</div>
<ReportCard
v-for="report in (showClosed ? reports : openReports)"
:key="report.id"
v-bind="report"
/>
</div>
</div>
</div>
</template>
<script src="./reports_tab.js"></script>
<style src="./reports_tab.scss" lang="scss"></style>

View file

@ -1,6 +1,6 @@
<template> <template>
<div <div
v-if="federationPolicy" v-if="hasInstanceSpecificPolicies"
class="mrf-transparency-panel" class="mrf-transparency-panel"
> >
<div class="panel panel-default base01-background"> <div class="panel panel-default base01-background">

View file

@ -33,11 +33,6 @@ library.add(
) )
const NavPanel = { const NavPanel = {
created () {
if (this.currentUser && this.currentUser.locked) {
this.$store.dispatch('startFetchingFollowRequests')
}
},
components: { components: {
TimelineMenuContent TimelineMenuContent
}, },
@ -54,11 +49,13 @@ const NavPanel = {
computed: { computed: {
...mapState({ ...mapState({
currentUser: state => state.users.currentUser, currentUser: state => state.users.currentUser,
followRequestCount: state => state.api.followRequests.length,
privateMode: state => state.instance.private, privateMode: state => state.instance.private,
federating: state => state.instance.federating federating: state => state.instance.federating
}), }),
...mapGetters(['unreadAnnouncementCount']) ...mapGetters(['unreadAnnouncementCount']),
followRequestCount () {
return this.$store.state.users.currentUser.follow_requests_count
}
} }
} }

View file

@ -5,6 +5,7 @@ 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 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 { 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'
@ -36,7 +37,9 @@ const Notification = {
return { return {
userExpanded: false, userExpanded: false,
betterShadow: this.$store.state.interface.browserSupport.cssFilter, betterShadow: this.$store.state.interface.browserSupport.cssFilter,
unmuted: false unmuted: false,
showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false
} }
}, },
props: [ 'notification' ], props: [ 'notification' ],
@ -46,7 +49,8 @@ const Notification = {
UserCard, UserCard,
Timeago, Timeago,
Status, Status,
RichContent RichContent,
ConfirmModal
}, },
methods: { methods: {
toggleUserExpanded () { toggleUserExpanded () {
@ -61,7 +65,26 @@ const Notification = {
toggleMute () { toggleMute () {
this.unmuted = !this.unmuted this.unmuted = !this.unmuted
}, },
showApproveConfirmDialog () {
this.showingApproveConfirmDialog = true
},
hideApproveConfirmDialog () {
this.showingApproveConfirmDialog = false
},
showDenyConfirmDialog () {
this.showingDenyConfirmDialog = true
},
hideDenyConfirmDialog () {
this.showingDenyConfirmDialog = false
},
approveUser () { approveUser () {
if (this.shouldConfirmApprove) {
this.showApproveConfirmDialog()
} else {
this.doApprove()
}
},
doApprove () {
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id }) this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id })
@ -71,13 +94,22 @@ const Notification = {
notification.type = 'follow' notification.type = 'follow'
} }
}) })
this.hideApproveConfirmDialog()
}, },
denyUser () { denyUser () {
if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog()
} else {
this.doDeny()
}
},
doDeny () {
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
.then(() => { .then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id }) this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
}) })
this.hideDenyConfirmDialog()
} }
}, },
computed: { computed: {
@ -107,6 +139,15 @@ const Notification = {
isStatusNotification () { isStatusNotification () {
return isStatusNotification(this.notification.type) return isStatusNotification(this.notification.type)
}, },
mergedConfig () {
return this.$store.getters.mergedConfig
},
shouldConfirmApprove () {
return this.mergedConfig.modalOnApproveFollow
},
shouldConfirmDeny () {
return this.mergedConfig.modalOnDenyFollow
},
...mapState({ ...mapState({
currentUser: state => state.users.currentUser currentUser: state => state.users.currentUser
}) })

View file

@ -231,6 +231,28 @@
</template> </template>
</div> </div>
</div> </div>
<teleport to="#modal">
<confirm-modal
v-if="showingApproveConfirmDialog"
:title="$t('user_card.approve_confirm_title')"
:confirm-text="$t('user_card.approve_confirm_accept_button')"
:cancel-text="$t('user_card.approve_confirm_cancel_button')"
@accepted="doApprove"
@cancelled="hideApproveConfirmDialog"
>
{{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
<confirm-modal
v-if="showingDenyConfirmDialog"
:title="$t('user_card.deny_confirm_title')"
:confirm-text="$t('user_card.deny_confirm_accept_button')"
:cancel-text="$t('user_card.deny_confirm_cancel_button')"
@accepted="doDeny"
@cancelled="hideDenyConfirmDialog"
>
{{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
</teleport>
</div> </div>
</template> </template>

View file

@ -1,4 +1,4 @@
import PinchZoom from '@kazvmoe-infra/pinch-zoom-element' import PinchZoom from '@floatingghost/pinch-zoom-element'
export default { export default {
methods: { methods: {

View file

@ -13,6 +13,7 @@ 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 {
@ -55,6 +56,15 @@ const pxStringToNumber = (str) => {
const PostStatusForm = { const PostStatusForm = {
props: [ props: [
'statusId',
'statusText',
'statusIsSensitive',
'statusPoll',
'statusFiles',
'statusMediaDescriptions',
'statusScope',
'statusContentType',
'statusLanguage',
'replyTo', 'replyTo',
'quoteId', 'quoteId',
'repliedUser', 'repliedUser',
@ -63,6 +73,7 @@ const PostStatusForm = {
'subject', 'subject',
'disableSubject', 'disableSubject',
'disableScopeSelector', 'disableScopeSelector',
'disableVisibilitySelector',
'disableNotice', 'disableNotice',
'disableLockWarning', 'disableLockWarning',
'disablePolls', 'disablePolls',
@ -77,7 +88,8 @@ const PostStatusForm = {
'fileLimit', 'fileLimit',
'submitOnEnter', 'submitOnEnter',
'emojiPickerPlacement', 'emojiPickerPlacement',
'optimisticPosting' 'optimisticPosting',
'isRedraft'
], ],
emits: [ emits: [
'posted', 'posted',
@ -118,7 +130,36 @@ const PostStatusForm = {
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser) statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
} }
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject } = this.$store.getters.mergedConfig const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage } = this.$store.getters.mergedConfig
let statusParams = {
spoilerText: this.subject || '',
status: statusText,
sensitiveByDefault,
nsfw: !!sensitiveByDefault,
files: [],
poll: {},
mediaDescriptions: {},
visibility: this.suggestedVisibility(),
language: interfaceLanguage,
contentType
}
if (this.statusId || this.isRedraft) {
const statusContentType = this.statusContentType || contentType
statusParams = {
spoilerText: this.subject || '',
status: this.statusText || '',
sensitiveIfSubject,
nsfw: this.statusIsSensitive || (sensitiveIfSubject && this.subject) || !!sensitiveByDefault,
files: this.statusFiles || [],
poll: this.statusPoll || {},
mediaDescriptions: this.statusMediaDescriptions || {},
visibility: this.statusScope || this.suggestedVisibility(),
language: this.statusLanguage || interfaceLanguage,
contentType: statusContentType
}
}
return { return {
dropFiles: [], dropFiles: [],
@ -126,17 +167,7 @@ const PostStatusForm = {
error: null, error: null,
posting: false, posting: false,
highlighted: 0, highlighted: 0,
newStatus: { newStatus: statusParams,
spoilerText: this.subject || '',
status: statusText,
sensitiveIfSubject,
nsfw: !!sensitiveByDefault,
files: [],
poll: {},
mediaDescriptions: {},
visibility: this.suggestedVisibility(),
contentType
},
caret: 0, caret: 0,
pollFormVisible: false, pollFormVisible: false,
showDropIcon: 'hide', showDropIcon: 'hide',
@ -154,9 +185,6 @@ const PostStatusForm = {
userDefaultScope () { userDefaultScope () {
return this.$store.state.users.currentUser.default_scope return this.$store.state.users.currentUser.default_scope
}, },
showAllScopes () {
return !this.mergedConfig.minimalScopesMode
},
emojiUserSuggestor () { emojiUserSuggestor () {
return suggestor({ return suggestor({
emoji: [ emoji: [
@ -198,9 +226,6 @@ const PostStatusForm = {
isOverLengthLimit () { isOverLengthLimit () {
return this.hasStatusLengthLimit && (this.charactersLeft < 0) return this.hasStatusLengthLimit && (this.charactersLeft < 0)
}, },
minimalScopesMode () {
return this.$store.state.instance.minimalScopesMode
},
alwaysShowSubject () { alwaysShowSubject () {
return this.mergedConfig.alwaysShowSubjectInput return this.mergedConfig.alwaysShowSubjectInput
}, },
@ -232,10 +257,16 @@ const PostStatusForm = {
uploadFileLimitReached () { uploadFileLimitReached () {
return this.newStatus.files.length >= this.fileLimit return this.newStatus.files.length >= this.fileLimit
}, },
isEdit () {
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== '' && !this.isRedraft
},
...mapGetters(['mergedConfig']), ...mapGetters(['mergedConfig']),
...mapState({ ...mapState({
mobileLayout: state => state.interface.mobileLayout mobileLayout: state => state.interface.mobileLayout
}) }),
isoLanguages () {
return iso6391.getAllCodes();
}
}, },
watch: { watch: {
'newStatus': { 'newStatus': {
@ -258,6 +289,7 @@ const PostStatusForm = {
files: [], files: [],
visibility: newStatus.visibility, visibility: newStatus.visibility,
contentType: newStatus.contentType, contentType: newStatus.contentType,
language: newStatus.language,
poll: {}, poll: {},
mediaDescriptions: {} mediaDescriptions: {}
} }
@ -317,6 +349,7 @@ const PostStatusForm = {
inReplyToStatusId: this.replyTo, inReplyToStatusId: this.replyTo,
quoteId: this.quoteId, quoteId: this.quoteId,
contentType: newStatus.contentType, contentType: newStatus.contentType,
language: newStatus.language,
poll, poll,
idempotencyKey: this.idempotencyKey idempotencyKey: this.idempotencyKey
} }
@ -351,6 +384,7 @@ const PostStatusForm = {
inReplyToStatusId: this.replyTo, inReplyToStatusId: this.replyTo,
quoteId: this.quoteId, quoteId: this.quoteId,
contentType: newStatus.contentType, contentType: newStatus.contentType,
language: newStatus.language,
poll: {}, poll: {},
preview: true preview: true
}).then((data) => { }).then((data) => {
@ -388,7 +422,7 @@ const PostStatusForm = {
addMediaFile (fileInfo) { addMediaFile (fileInfo) {
this.newStatus.files.push(fileInfo) this.newStatus.files.push(fileInfo)
if (this.newStatus.sensitiveIfSubject && this.newStatus.spoilerText !== '') { if (this.$store.getters.mergedConfig.sensitiveIfSubject && this.newStatus.spoilerText !== '') {
this.newStatus.nsfw = true this.newStatus.nsfw = true
} }
this.$emit('resize', { delayed: true }) this.$emit('resize', { delayed: true })
@ -468,7 +502,7 @@ const PostStatusForm = {
}) })
}, },
onSubjectInput (e) { onSubjectInput (e) {
if (this.newStatus.sensitiveIfSubject) { if (this.$store.getters.mergedConfig.sensitiveIfSubject) {
this.newStatus.nsfw = true this.newStatus.nsfw = true
} }
}, },
@ -617,12 +651,10 @@ const PostStatusForm = {
if (this.copyMessageScope === 'direct') { if (this.copyMessageScope === 'direct') {
return this.copyMessageScope return this.copyMessageScope
} }
if (this.$store.getters.mergedConfig.scopeCopy) {
if (this.copyMessageScope !== 'public' && this.$store.state.users.currentUser.default_scope !== 'private') { if (this.copyMessageScope !== 'public' && this.$store.state.users.currentUser.default_scope !== 'private') {
return this.copyMessageScope return this.copyMessageScope
} }
} }
}
return this.$store.state.users.currentUser.default_scope return this.$store.state.users.currentUser.default_scope
} }
} }

View file

@ -66,6 +66,13 @@
<span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span> <span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
<span v-else>{{ $t('post_status.direct_warning_to_all') }}</span> <span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
</p> </p>
<div
v-if="isEdit"
class="visibility-notice edit-warning"
>
<p>{{ $t('post_status.edit_remote_warning') }}</p>
<p>{{ $t('post_status.edit_unsupported_warning') }}</p>
</div>
<div <div
v-if="!disablePreview" v-if="!disablePreview"
class="preview-heading faint" class="preview-heading faint"
@ -180,13 +187,30 @@
class="visibility-tray" class="visibility-tray"
> >
<scope-selector <scope-selector
:show-all="showAllScopes" v-if="!disableVisibilitySelector"
:user-default="userDefaultScope" :user-default="userDefaultScope"
:original-scope="copyMessageScope" :original-scope="copyMessageScope"
:initial-scope="newStatus.visibility" :initial-scope="newStatus.visibility"
:on-scope-change="changeVis" :on-scope-change="changeVis"
/> />
<div
class="language-selector"
>
<Select
id="post-language"
v-model="newStatus.language"
class="form-control"
>
<option
v-for="language in isoLanguages"
:key="language"
:value="language"
>
{{ language }}
</option>
</Select>
</div>
<div <div
v-if="postFormats.length > 1" v-if="postFormats.length > 1"
class="text-format" class="text-format"
@ -420,6 +444,16 @@
align-items: baseline; align-items: baseline;
} }
.visibility-notice.edit-warning {
> :first-child {
margin-top: 0;
}
> :last-child {
margin-bottom: 0;
}
}
.media-upload-icon, .poll-icon, .emoji-icon { .media-upload-icon, .poll-icon, .emoji-icon {
font-size: 1.85em; font-size: 1.85em;
line-height: 1.1; line-height: 1.1;

View file

@ -28,7 +28,8 @@ const PostStatusModal = {
}, },
watch: { watch: {
params (newVal, oldVal) { params (newVal, oldVal) {
if (get(newVal, 'repliedUser.id') !== get(oldVal, 'repliedUser.id')) { if (get(newVal, 'repliedUser.id') !== get(oldVal, 'repliedUser.id') ||
get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
this.resettingForm = true this.resettingForm = true
this.$nextTick(() => { this.$nextTick(() => {
this.resettingForm = false this.resettingForm = false

View file

@ -79,8 +79,16 @@ const registration = {
if (!this.v$.$invalid) { if (!this.v$.$invalid) {
try { try {
await this.signUp(this.user) const data = await this.signUp(this.user)
if (data.me) {
this.$router.push({ name: 'friends' }) this.$router.push({ name: 'friends' })
} else if (data.identifier === 'awaiting_approval') {
this.$router.push({ name: 'registration-request-sent' })
} else if (data.identifier === 'missing_confirmed_email') {
this.$router.push({ name: 'awaiting-email-confirmation' })
} else {
console.warn('Unknown response from sign up', data)
}
} catch (error) { } catch (error) {
console.warn('Registration failed: ', error) console.warn('Registration failed: ', error)
this.setCaptcha() this.setCaptcha()

View file

@ -177,6 +177,7 @@
<div <div
v-if="accountApprovalRequired" v-if="accountApprovalRequired"
class="form-group" class="form-group"
:class="{ 'form-group--error': v$.user.reason.$error }"
> >
<label <label
class="form--label" class="form--label"

View file

@ -0,0 +1,4 @@
export default {
computed: {
}
}

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