Compare commits

...

119 Commits

Author SHA1 Message Date
floatingghost 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
floatingghost 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
floatingghost 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
floatingghost 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
FloatingGhost c39332c1bf Revert "Revert "use v1 urls""
This reverts commit 8c6cf86de3.
2022-12-14 09:39:01 +00:00
FloatingGhost 8c6cf86de3 Revert "use v1 urls"
This reverts commit 909271c764.
2022-12-14 09:38:46 +00:00
FloatingGhost 909271c764 use v1 urls 2022-12-14 09:38:07 +00:00
anna 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
FloatingGhost 9bf310d509 bump version 2022-12-10 14:51:08 +00:00
FloatingGhost 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
floatingghost 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
floatingghost 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
floatingghost 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
floatingghost d3280c4ab3 Add issue template 2022-12-07 22:31:29 +00:00
FloatingGhost abc75c360b Ensure only content gets clipped 2022-12-07 11:01:58 +00:00
FloatingGhost a8e119b0f1 Merge branch 'develop' of akkoma.dev:AkkomaGang/pleroma-fe into develop 2022-12-06 15:56:06 +00:00
FloatingGhost 17e574b173 Move theme apply/reset to new row
Fixes #225
2022-12-06 15:55:39 +00:00
floatingghost 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
FloatingGhost b68e968bf9 Add ability to include custom CSS 2022-12-06 15:26:16 +00:00
floatingghost 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
astra akari 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
floatingghost 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
FloatingGhost a3e490edcd use cutout of 512 logo 2022-12-02 12:00:30 +00:00
FloatingGhost f8f5e1c89b fix SW path 2022-12-02 11:57:45 +00:00
FloatingGhost e132814478 Register serviceworker 2022-12-02 11:56:15 +00:00
FloatingGhost 6af1df8bef Add logo files 2022-12-02 11:27:24 +00:00
floatingghost 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
floatingghost 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
FloatingGhost 0a01a2bdf0 Add post expiry inputs 2022-11-28 12:08:18 +00:00
darkkirb 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
floatingghost 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
floatingghost 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
floatingghost 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
FloatingGhost b1f41add0e Don't error out if we can't fetch reports 2022-11-21 17:08:53 +00:00
floatingghost 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
Beefox 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
floatingghost 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
floatingghost 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
Mergan 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
Beefox fcbbbad8d4 Allow for searching unicode emoji via inputting emoji
(This is needed for the react menu)
2022-11-21 01:04:00 -08:00
FloatingGhost 39b6b0b49f Merge remote-tracking branch 'origin/translations' into develop 2022-11-20 22:26:32 +00:00
FloatingGhost 867a86d887 Fix build 2022-11-20 22:26:16 +00:00
FloatingGhost 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
astra akari 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
FloatingGhost 169282ea42 rely on backend mfm parsing 2022-11-15 15:54:16 +00:00
floatingghost 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
FloatingGhost 0770981a20 Ensure we don't fail if translation is disabled 2022-11-12 19:06:39 +00:00
floatingghost 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
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
FloatingGhost c7200b2234 make emoji reactions in notifications bar wider 2022-11-11 16:47:25 +00:00
FloatingGhost 98074ed90d make desc overflow a scroll 2022-11-11 16:20:41 +00:00
floatingghost 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
FloatingGhost 4b5536ae68 Merge branch 'develop' of akkoma.dev:AkkomaGang/pleroma-fe into develop 2022-11-11 16:05:10 +00:00
FloatingGhost e44462b1d5 add "no reports" message 2022-11-11 16:05:02 +00:00
Ngô Ngọc Đức Huy 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
floatingghost 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
FloatingGhost e77931d68c Use correct sensitiveIfSubject key 2022-11-10 12:06:59 +00:00
floatingghost 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
FloatingGhost 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
FloatingGhost 8713f1870f use correct key for languages
fixes #196
2022-11-08 12:25:47 +00:00
floatingghost 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
sn0w 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
floatingghost 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
floatingghost 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
floatingghost 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
floatingghost 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
FloatingGhost 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
floatingghost 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
FloatingGhost 469063ff52 Debounce word filters
Fixes #182
2022-11-02 22:08:58 +00:00
FloatingGhost d8643b5b4a Take instance stopGifs value 2022-10-29 22:08:25 +01:00
FloatingGhost 04c744e764 Stop stopGifs option from instance 2022-10-29 21:56:58 +01:00
solidsanek 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
FloatingGhost 2e00c19074 Merge remote-tracking branch 'origin/translations' into develop 2022-10-08 11:56:09 +01:00
FloatingGhost 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
floatingghost 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
floatingghost 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
Mergan 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
floatingghost 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
FloatingGhost 87683052e8 add hacky reply filtering, turn off streaming by default 2022-09-20 16:27:17 +01:00
FloatingGhost 42e48348ae add note about bubble timeline in docs 2022-09-20 15:17:51 +01:00
floatingghost 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
floatingghost 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
Norm bf1debdeb6
Move remote interaction chnagelog entry to "Unreleased" 2022-09-19 18:39:36 -04:00
Sol Fisher Romanoff b936506f47
Hide mentions of bubble timeline if empty 2022-09-14 21:44:29 +03:00
138 changed files with 6740 additions and 6754 deletions

View File

@ -1,17 +1,17 @@
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
parser: '@babel/eslint-parser',
sourceType: 'module'
},
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: [
'standard',
'plugin:vue/recommended'
],
// required to lint *.vue files
plugins: [
'vue'
'vue',
'import'
],
// add your custom rules here
rules: {
@ -23,6 +23,8 @@ module.exports = {
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'vue/require-prop-types': 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
docs/site/
.vscode/

View File

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

View File

@ -3,17 +3,17 @@ pipeline:
when:
event:
- pull_request
image: node:16
image: node:18
commands:
- yarn
- yarn lint
- yarn stylelint
#- yarn stylelint
test:
when:
event:
- pull_request
image: node:16
image: node:18
commands:
- apt update
- apt install firefox-esr -y --no-install-recommends
@ -27,7 +27,7 @@ pipeline:
branch:
- develop
- stable
image: node:16
image: node:18
commands:
- yarn
- yarn build
@ -39,7 +39,7 @@ pipeline:
branch:
- develop
- stable
image: node:16
image: node:18
secrets:
- SCW_ACCESS_KEY
- SCW_SECRET_KEY

View File

@ -3,6 +3,11 @@ 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/).
## 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.
@ -62,7 +67,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Ability to rearrange order of attachments when uploading
- Enabled users to zoom and pan images in media viewer with mouse and touch
- Added frontend ui for account migration
- Implemented remote interaction with statuses
## [2.4.2] - 2022-01-09

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

@ -29,18 +29,6 @@ var devMiddleware = require('webpack-dev-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
Object.keys(proxyTable).forEach(function (context) {

View File

@ -2,8 +2,6 @@ var path = require('path')
var config = require('../config')
var utils = require('./utils')
var projectRoot = path.resolve(__dirname, '../')
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
var CopyPlugin = require('copy-webpack-plugin');
var { VueLoaderPlugin } = require('vue-loader')
var env = process.env.NODE_ENV
@ -20,6 +18,7 @@ module.exports = {
app: './src/main.js'
},
output: {
hashFunction: "sha256", // Workaround for builds with OpenSSL 3.
path: config.build.assetsRoot,
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
filename: '[name].js'
@ -34,6 +33,9 @@ module.exports = {
modules: [
path.join(__dirname, '../node_modules')
],
fallback: {
"url": require.resolve("url/"),
},
alias: {
'static': path.resolve(__dirname, '../static'),
'src': path.resolve(__dirname, '../src'),
@ -116,23 +118,6 @@ module.exports = {
]
},
plugins: [
new ServiceWorkerWebpackPlugin({
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,
},
})
new VueLoaderPlugin()
]
}

View File

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

View File

@ -2,7 +2,8 @@ var path = require('path')
var config = require('../config')
var utils = require('./utils')
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 MiniCssExtractPlugin = require('mini-css-extract-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
@ -19,7 +20,7 @@ var webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true })
},
devtool: config.build.productionSourceMap ? '#source-map' : false,
devtool: 'source-map',
optimization: {
minimize: true,
splitChunks: {
@ -32,6 +33,11 @@ var webpackConfig = merge(baseWebpackConfig, {
chunkFilename: utils.assetsPath('js/[name].[chunkhash].js')
},
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
new webpack.DefinePlugin({
'process.env': env,
@ -62,7 +68,7 @@ var webpackConfig = merge(baseWebpackConfig, {
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
chunksSortMode: 'auto'
}),
// split vendor js into its own file
// 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')
module.exports = merge(prodEnv, {

View File

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

View File

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

View File

@ -70,9 +70,6 @@ Default post formatting option (markdown/bbcode/plaintext/etc...)
### `redirectRootNoLogin`, `redirectRootLogin`
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`
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.
- **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
- **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.
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,17 +3,19 @@
<head>
<meta charset="utf-8">
<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/animation.css">
<link rel="stylesheet" href="/static/font/tiresias.css">
<link rel="stylesheet" href="/static/font/css/lato.css">
<link rel="stylesheet" href="/static/mfm.css">
<link rel="stylesheet" href="/static/custom.css">
<!--server-generated-meta-->
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="manifest" href="/manifest.json">
</head>
<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="modal"></div>
<!-- built files will be auto injected -->

View File

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

View File

@ -5,6 +5,7 @@ import FeaturesPanel from './components/features_panel/features_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 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 MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
import MobileNav from './components/mobile_nav/mobile_nav.vue'
@ -33,6 +34,7 @@ export default {
MobileNav,
DesktopNav,
SettingsModal,
ModModal,
UserReportingModal,
PostStatusModal,
EditStatusModal,

View File

@ -61,7 +61,7 @@
<EditStatusModal v-if="editingAvailable" />
<StatusHistoryModal v-if="editingAvailable" />
<SettingsModal />
<UpdateNotification />
<ModModal />
<GlobalNoticeList />
</div>
</template>

View File

@ -148,7 +148,9 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('showWiderShortcuts')
copyInstanceOption('showNavShortcuts')
copyInstanceOption('showPanelNavShortcuts')
copyInstanceOption('stopGifs')
copyInstanceOption('logo')
copyInstanceOption('conversationDisplay')
store.dispatch('setInstanceOption', {
name: 'logoMask',
@ -169,10 +171,8 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('redirectRootNoLogin')
copyInstanceOption('redirectRootLogin')
copyInstanceOption('showInstanceSpecificPanel')
copyInstanceOption('minimalScopesMode')
copyInstanceOption('hideMutedPosts')
copyInstanceOption('collapseMessageWithSubject')
copyInstanceOption('scopeCopy')
copyInstanceOption('subjectLineBehavior')
copyInstanceOption('postContentType')
copyInstanceOption('alwaysShowSubjectInput')
@ -396,9 +396,9 @@ const afterStoreSetup = async ({ store, i18n }) => {
// Start fetching things that don't need to block the UI
store.dispatch('fetchMutes')
store.dispatch('startFetchingAnnouncements')
store.dispatch('startFetchingReports')
getTOS({ store })
getStickers({ store })
store.dispatch('getSupportedTranslationlanguages')
const router = createRouter({
history: createWebHistory(),

View File

@ -22,6 +22,8 @@ import Lists from 'components/lists/lists.vue'
import ListTimeline from 'components/list_timeline/list_timeline.vue'
import ListEdit from 'components/list_edit/list_edit.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) => {
const validateAuthenticatedRoute = (to, from, next) => {
@ -62,6 +64,8 @@ export default (store) => {
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{ 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: 'registration-token', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },

View File

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

View File

@ -3,7 +3,7 @@
<instance-specific-panel v-if="showInstanceSpecificPanel" />
<staff-panel />
<terms-of-service-panel />
<LocalBubblePanel />
<LocalBubblePanel v-if="showLocalBubblePanel" />
<MRFTransparencyPanel />
<features-panel v-if="showFeaturesPanel" />
</div>

View File

@ -26,6 +26,9 @@ const AccountActions = {
ConfirmModal
},
methods: {
refetchRelationship () {
return this.$store.dispatch('fetchUserRelationship', this.user.id)
},
showConfirmBlock () {
this.showingConfirmBlock = true
},
@ -52,8 +55,19 @@ const AccountActions = {
unblockUser () {
this.$store.dispatch('unblockUser', this.user.id)
},
removeUserFromFollowers () {
this.$store.dispatch('removeUserFromFollowers', this.user.id)
},
reportUser () {
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: {

View File

@ -28,6 +28,13 @@
class="dropdown-divider"
/>
</template>
<button
v-if="relationship.followed_by"
class="btn button-default btn-block dropdown-item"
@click="removeUserFromFollowers"
>
{{ $t('user_card.remove_follower') }}
</button>
<button
v-if="relationship.blocking"
class="btn button-default btn-block dropdown-item"
@ -48,6 +55,20 @@
>
{{ $t('user_card.report') }}
</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>
</template>
<template v-slot:trigger>

View File

@ -227,7 +227,7 @@ const Attachment = {
this.$emit('resize', newHeight)
},
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;
padding-top: 0.5em;
z-index: 1;
max-height: 50%;
p {
flex: 1;
@ -36,7 +37,7 @@
white-space: pre-line;
word-break: break-word;
text-overflow: ellipsis;
overflow: hidden;
overflow: scroll;
}
&.-static {

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

@ -16,7 +16,8 @@ import {
faUsers,
faCommentMedical,
faBookmark,
faInfoCircle
faInfoCircle,
faUserTie
} from '@fortawesome/free-solid-svg-icons'
library.add(
@ -34,7 +35,8 @@ library.add(
faUsers,
faCommentMedical,
faBookmark,
faInfoCircle
faInfoCircle,
faUserTie
)
export default {
@ -96,8 +98,15 @@ export default {
logoLeft () { return this.$store.state.instance.logoLeft },
currentUser () { return this.$store.state.users.currentUser },
privateMode () { return this.$store.state.instance.private },
federating () { return this.$store.state.instance.federating },
shouldConfirmLogout () {
return this.$store.getters.mergedConfig.modalOnLogout
},
showBubbleTimeline () {
return this.$store.state.instance.localBubbleInstances.length > 0
},
restrictedTimelines () {
return this.$store.state.instance.restrict_unauthenticated.timelines
}
},
methods: {
@ -109,6 +118,9 @@ export default {
},
openSettingsModal () {
this.$store.dispatch('openSettingsModal')
},
openModModal () {
this.$store.dispatch('openModModal')
}
}
}

View File

@ -44,6 +44,7 @@
/>
</router-link>
<router-link
v-if="currentUser || !(privateMode || restrictedTimelines.public)"
:to="{ name: 'public-timeline' }"
class="nav-icon"
>
@ -55,7 +56,7 @@
/>
</router-link>
<router-link
v-if="currentUser"
v-if="currentUser && showBubbleTimeline"
:to="{ name: 'bubble-timeline' }"
class="nav-icon"
>
@ -67,6 +68,7 @@
/>
</router-link>
<router-link
v-if="federating && (currentUser || !(privateMode || restrictedTimelines.federated))"
:to="{ name: 'public-external-timeline' }"
class="nav-icon"
>
@ -151,6 +153,18 @@
:title="$t('nav.preferences')"
/>
</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
v-if="currentUser && currentUser.role === 'admin'"
href="/pleroma/admin/#/login-pleroma"

View File

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

View File

@ -42,7 +42,7 @@
:class="{ highlighted: index === highlighted }"
@click.stop.prevent="onClick($event, suggestion)"
>
<span class="image">
<span v-if="!suggestion.mfm" class="image">
<img
v-if="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
* data: object providing source information for specific types of suggestions:
@ -21,6 +24,10 @@ export default data => {
if (firstChar === '@' && usersCurry) {
return usersCurry(input)
}
if (firstChar === '$') {
return MFM_TAGS
.filter(({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1)
}
return []
}
}

View File

@ -62,6 +62,10 @@ const EmojiPicker = {
this.scrolledGroup(target)
this.triggerLoadMore(target)
},
onWheel (e) {
e.preventDefault()
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
},
highlight (key) {
this.setShowStickers(false)
this.activeGroup = key
@ -138,7 +142,7 @@ const EmojiPicker = {
if (this.keyword === '') return list
const regex = new RegExp(escapeRegExp(trim(this.keyword)), 'i')
return list.filter(emoji => {
return regex.test(emoji.displayText)
return (regex.test(emoji.displayText) || (!emoji.imageUrl && emoji.replacement === this.keyword))
})
}
},

View File

@ -1,5 +1,25 @@
@import '../../_variables.scss';
.Notification {
.emoji-picker {
min-width: 160%;
width: 150%;
overflow: hidden;
left: -70%;
max-width: 100%;
@media (min-width: 800px) and (max-width: 1300px) {
left: -50%;
min-width: 50%;
max-width: 130%;
}
@media (max-width: 800px) {
left: -10%;
min-width: 50%;
max-width: 130%;
}
}
}
.emoji-picker {
display: flex;
flex-direction: column;

View File

@ -1,7 +1,11 @@
<template>
<div class="emoji-picker panel panel-default panel-body">
<div class="heading">
<span class="emoji-tabs">
<span
class="emoji-tabs"
@wheel="onWheel"
ref="emoji-tabs"
>
<span
v-for="group in emojis"
:key="group.id"

View File

@ -92,7 +92,7 @@
}
}
.picked-reaction {
.button-default.picked-reaction {
border: 1px solid var(--accent, $fallback--link);
margin-left: -1px; // offset the border, can't use inset shadows either
margin-right: calc(0.5em - 1px);

View File

@ -8,7 +8,8 @@ import {
faThumbtack,
faShareAlt,
faExternalLinkAlt,
faHistory
faHistory,
faFilePen
} from '@fortawesome/free-solid-svg-icons'
import {
faBookmark as faBookmarkReg,
@ -24,7 +25,8 @@ library.add(
faShareAlt,
faExternalLinkAlt,
faFlag,
faHistory
faHistory,
faFilePen
)
const ExtraButtons = {
@ -36,7 +38,8 @@ const ExtraButtons = {
data () {
return {
expanded: false,
showingDeleteDialog: false
showingDeleteDialog: false,
showingRedraftDialog: false
}
},
methods: {
@ -122,6 +125,34 @@ const ExtraButtons = {
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,
statusContentType: data.content_type
}))
this.doDeleteStatus()
},
showRedraftStatusConfirmDialog () {
this.showingRedraftDialog = true
},
hideRedraftStatusConfirmDialog () {
this.showingRedraftDialog = false
}
},
computed: {

View File

@ -95,6 +95,17 @@
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
v-if="canDelete"
class="button-default dropdown-item dropdown-item-icon"
@ -179,6 +190,16 @@
>
{{ $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>
</Popover>

View File

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

View File

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

View File

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

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

@ -86,7 +86,8 @@ const PostStatusForm = {
'fileLimit',
'submitOnEnter',
'emojiPickerPlacement',
'optimisticPosting'
'optimisticPosting',
'isRedraft'
],
emits: [
'posted',
@ -141,13 +142,13 @@ const PostStatusForm = {
contentType
}
if (this.statusId) {
if (this.statusId || this.isRedraft) {
const statusContentType = this.statusContentType || contentType
statusParams = {
spoilerText: this.subject || '',
status: this.statusText || '',
sensitiveIfSubject,
nsfw: this.statusIsSensitive || !!sensitiveByDefault,
nsfw: this.statusIsSensitive || (sensitiveIfSubject && this.subject) || !!sensitiveByDefault,
files: this.statusFiles || [],
poll: this.statusPoll || {},
mediaDescriptions: this.statusMediaDescriptions || {},
@ -180,9 +181,6 @@ const PostStatusForm = {
userDefaultScope () {
return this.$store.state.users.currentUser.default_scope
},
showAllScopes () {
return !this.mergedConfig.minimalScopesMode
},
emojiUserSuggestor () {
return suggestor({
emoji: [
@ -224,9 +222,6 @@ const PostStatusForm = {
isOverLengthLimit () {
return this.hasStatusLengthLimit && (this.charactersLeft < 0)
},
minimalScopesMode () {
return this.$store.state.instance.minimalScopesMode
},
alwaysShowSubject () {
return this.mergedConfig.alwaysShowSubjectInput
},
@ -259,7 +254,7 @@ const PostStatusForm = {
return this.newStatus.files.length >= this.fileLimit
},
isEdit () {
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== ''
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== '' && !this.isRedraft
},
...mapGetters(['mergedConfig']),
...mapState({
@ -417,7 +412,7 @@ const PostStatusForm = {
addMediaFile (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.$emit('resize', { delayed: true })
@ -497,7 +492,7 @@ const PostStatusForm = {
})
},
onSubjectInput (e) {
if (this.newStatus.sensitiveIfSubject) {
if (this.$store.getters.mergedConfig.sensitiveIfSubject) {
this.newStatus.nsfw = true
}
},
@ -646,10 +641,8 @@ const PostStatusForm = {
if (this.copyMessageScope === 'direct') {
return this.copyMessageScope
}
if (this.$store.getters.mergedConfig.scopeCopy) {
if (this.copyMessageScope !== 'public' && this.$store.state.users.currentUser.default_scope !== 'private') {
return this.copyMessageScope
}
if (this.copyMessageScope !== 'public' && this.$store.state.users.currentUser.default_scope !== 'private') {
return this.copyMessageScope
}
}
return this.$store.state.users.currentUser.default_scope

View File

@ -188,7 +188,6 @@
>
<scope-selector
v-if="!disableVisibilitySelector"
:show-all="showAllScopes"
:user-default="userDefaultScope"
:original-scope="copyMessageScope"
:initial-scope="newStatus.visibility"

View File

@ -28,7 +28,8 @@ const PostStatusModal = {
},
watch: {
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.$nextTick(() => {
this.resettingForm = false

View File

@ -79,8 +79,16 @@ const registration = {
if (!this.v$.$invalid) {
try {
await this.signUp(this.user)
this.$router.push({ name: 'friends' })
const data = await this.signUp(this.user)
if (data.me) {
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) {
console.warn('Registration failed: ', error)
this.setCaptcha()

View File

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

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.request_sent_title') }}</h4>
</div>
<div class="panel-body">
<p>{{ $t('registration.request_sent') }}</p>
</div>
</div>
</template>
<script src="./registration_request_sent.js"></script>

View File

@ -0,0 +1,25 @@
export default {
props: ['relationship'],
data () {
return {
inProgress: false
}
},
computed: {
label () {
if (this.inProgress) {
return this.$t('user_card.follow_progress')
} else {
return this.$t('user_card.remove_follower')
}
}
},
methods: {
onClick () {
this.inProgress = true
this.$store.dispatch('removeUserFromFollowers', this.relationship.id).then(() => {
this.inProgress = false
})
}
}
}

View File

@ -0,0 +1,13 @@
<template>
<button
class="btn button-default follow-button"
:class="{ toggled: inProgress }"
:disabled="inProgress"
:title="$t('user_card.remove_follower')"
@click="onClick"
>
{{ label }}
</button>
</template>
<script src="./remove_follower_button.js"></script>

View File

@ -2,8 +2,6 @@ import { unescape, flattenDeep } from 'lodash'
import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_converter/utility.service.js'
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
import { marked } from 'marked'
import markedMfm from 'marked-mfm'
import StillImage from 'src/components/still-image/still-image.vue'
import MentionsLine, { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.vue'
import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue'
@ -123,51 +121,6 @@ export default {
}
}
const renderMisskeyMarkdown = (content) => {
// Untangle code blocks from <br> tags and other html encodings
const codeblocks = content.match(/(<br\/>)?(~~~|```)\w*<br\/>.+?<br\/>\2\1?/g)
if (codeblocks) {
codeblocks.forEach((pre) => {
content = content.replace(pre,
pre.replaceAll('<br/>', '\n')
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot', '"')
.replaceAll('&#39;', "'")
)
})
}
marked.use(markedMfm, {
mangle: false,
gfm: false,
breaks: true
})
const mfmHtml = document.createElement('template')
mfmHtml.innerHTML = marked.parse(content)
// Add options with set values to CSS
if (mfmHtml.content.firstChild) {
Array.from(mfmHtml.content.firstChild.getElementsByClassName('mfm')).map((el) => {
if (el.dataset.speed) {
el.style.animationDuration = el.dataset.speed
}
if (el.dataset.deg) {
el.style.transform = `rotate(${el.dataset.deg}deg)`
}
if (Array.from(el.classList).includes('_mfm_font_')) {
const font = Object.keys(el.dataset)[0]
if (['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'].includes(font)) {
el.style.fontFamily = font
}
}
})
}
return mfmHtml.innerHTML
}
// Processor to use with html_tree_converter
const processItem = (item, index, array, what) => {
// Handle text nodes - just add emoji
@ -305,7 +258,7 @@ export default {
return item
}
const pass1 = convertHtmlToTree(this.mfm ? renderMisskeyMarkdown(html) : html).map(processItem)
const pass1 = convertHtmlToTree(html).map(processItem)
const pass2 = [...pass1].reverse().map(processItemReverse).reverse()
// DO NOT USE SLOTS they cause a re-render feedback loop here.
// slots updated -> rerender -> emit -> update up the tree -> rerender -> ...

View File

@ -13,6 +13,14 @@ library.add(
faLockOpen
)
const SCOPE_LEVELS = {
'direct': 0,
'private': 1,
'local': 2,
'unlisted': 2,
'public': 3
}
const ScopeSelector = {
props: [
'showAll',
@ -57,11 +65,15 @@ const ScopeSelector = {
},
methods: {
shouldShow (scope) {
return this.showAll ||
this.currentScope === scope ||
this.originalScope === scope ||
this.userDefault === scope ||
scope === 'direct'
if (!this.originalScope) {
return true
}
if (this.originalScope === 'local') {
return scope === 'direct' || scope === 'local'
}
return SCOPE_LEVELS[scope] <= SCOPE_LEVELS[this.originalScope]
},
changeVis (scope) {
this.currentScope = scope

View File

@ -16,7 +16,6 @@
class="fa-scale-110 fa-old-padding"
/>
</button>
{{ ' ' }}
<button
v-if="showPrivate"
class="button-unstyled scope"
@ -30,7 +29,6 @@
class="fa-scale-110 fa-old-padding"
/>
</button>
{{ ' ' }}
<button
v-if="showUnlisted"
class="button-unstyled scope"
@ -44,7 +42,6 @@
class="fa-scale-110 fa-old-padding"
/>
</button>
{{ ' ' }}
<button
v-if="showPublic"
class="button-unstyled scope"
@ -87,6 +84,7 @@
min-width: 1.3em;
min-height: 1.3em;
text-align: center;
margin-right: 0.4em;
&.selected svg {
color: $fallback--lightText;

View File

@ -32,6 +32,7 @@ const SearchBar = {
this.$emit('toggled', this.hidden)
this.$nextTick(() => {
if (!this.hidden) {
this.searchTerm = undefined
this.$refs.searchInput.focus()
}
})

View File

@ -73,6 +73,7 @@
.search-bar-input {
flex: 1 0 auto;
margin-left: 0.5em;
}
.cancel-search {

View File

@ -12,7 +12,8 @@ export default {
'path',
'disabled',
'options',
'expert'
'expert',
'hideDefaultLabel'
],
computed: {
pathDefault () {

View File

@ -16,7 +16,11 @@
:value="option.value"
>
{{ option.label }}
{{ option.value === defaultState ? $t('settings.instance_default_simple') : '' }}
<template
v-if="hideDefaultLabel !== true"
>
{{ option.value === defaultState ? $t('settings.instance_default_simple') : '' }}
</template>
</option>
</Select>
<ModifiedIndicator :changed="isChanged" />

View File

@ -19,7 +19,7 @@ const SharedComputedObject = () => ({
.map(key => [key, {
get () { return this.$store.getters.mergedConfig[key] },
set (value) {
this.$store.dispatch('setOption', { name: key, value })
this.$store.dispatch('setOption', { name: key, value, manual: true })
}
}])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
@ -27,7 +27,7 @@ const SharedComputedObject = () => ({
.map(key => ['serverSide_' + key, {
get () { return this.$store.state.serverSideConfig[key] },
set (value) {
this.$store.dispatch('setServerSideOption', { name: key, value })
this.$store.dispatch('setServerSideOption', { name: key, value, manual: true })
}
}])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),

View File

@ -175,7 +175,6 @@ const SettingsModal = {
return this.$store.state.config.expertLevel > 0
},
set (value) {
console.log(value)
this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 })
}
}

View File

@ -76,6 +76,10 @@
position: absolute;
right: 20px;
padding-right: 10px;
@media all and (max-width: 800px) {
display: none;
}
}
}
}

View File

@ -44,6 +44,10 @@
<div class="panel-body">
<SettingsModalContent v-if="modalOpenedOnce" />
</div>
<span
id="unscrolled-content"
class="extra-content"
/>
<div class="panel-footer settings-footer">
<Popover
class="export"
@ -53,7 +57,7 @@
:bound-to="{ x: 'container' }"
remove-padding
>
<template v-slot:trigger>
<template #trigger>
<button
class="btn button-default"
:title="$t('general.close')"
@ -65,7 +69,7 @@
/>
</button>
</template>
<template v-slot:content="{close}">
<template #content="{close}">
<div class="dropdown-menu">
<button
class="button-default dropdown-item dropdown-item-icon"
@ -103,14 +107,11 @@
<Checkbox
:model-value="!!expertLevel"
class="expertMode"
@update:modelValue="expertLevel = Number($event)"
>
{{ $t("settings.expert_mode") }}
</Checkbox>
<span
id="unscrolled-content"
class="extra-content"
/>
<button
v-if="currentUser"
class="button-default logout-button"

View File

@ -1,4 +1,4 @@
import { filter, trim } from 'lodash'
import { filter, trim, debounce } from 'lodash'
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
@ -27,13 +27,13 @@ const FilteringTab = {
get () {
return this.muteWordsStringLocal
},
set (value) {
set: debounce(function (value) {
this.muteWordsStringLocal = value
this.$store.dispatch('setOption', {
name: 'muteWords',
value: filter(value.split('\n'), (word) => trim(word).length > 0)
})
}
}, 500)
}
},
// Updating nested properties

View File

@ -8,11 +8,12 @@ import SharedComputedObject from '../helpers/shared_computed_object.js'
import ServerSideIndicator from '../helpers/server_side_indicator.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
faGlobe, faSync
} from '@fortawesome/free-solid-svg-icons'
library.add(
faGlobe
faGlobe,
faSync
)
const GeneralTab = {
@ -48,6 +49,8 @@ const GeneralTab = {
value: tab,
label: this.$t(`user_card.${tab}`)
})),
profilesExpanded: false,
newProfileName: '',
loopSilentAvailable:
// Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
@ -88,8 +91,26 @@ const GeneralTab = {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
}
},
settingsProfiles () {
return (this.$store.state.instance.settingsProfiles || [])
},
settingsProfile: {
get: function () { return this.$store.getters.mergedConfig.profile },
set: function (val) {
this.$store.dispatch('setOption', { name: 'profile', value: val })
this.$store.dispatch('getSettingsProfile')
}
},
settingsVersion () {
return this.$store.getters.mergedConfig.profileVersion
},
translationLanguages () {
return (this.$store.getters.mergedConfig.supportedTranslationLanguages.target || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
const langs = this.$store.state.instance.supportedTranslationLanguages
if (langs && langs.source) {
return langs.source.map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
}
return []
},
translationLanguage: {
get: function () { return this.$store.getters.mergedConfig.translationLanguage },
@ -105,6 +126,30 @@ const GeneralTab = {
},
setTranslationLanguage (value) {
this.$store.dispatch('setOption', { name: 'translationLanguage', value })
},
toggleExpandedSettings () {
this.profilesExpanded = !this.profilesExpanded
},
loadSettingsProfile (name) {
this.$store.commit('setOption', { name: 'profile', value: name })
this.$store.dispatch('getSettingsProfile', true)
},
createSettingsProfile () {
this.$store.dispatch('setOption', { name: 'profile', value: this.newProfileName })
this.$store.dispatch('setOption', { name: 'profileVersion', value: 1 })
this.$store.dispatch('syncSettings')
this.newProfileName = ''
},
forceSync () {
this.$store.dispatch('getSettingsProfile')
},
refreshProfiles () {
this.$store.dispatch('listSettingsProfiles')
},
deleteSettingsProfile (name) {
if (confirm(this.$t('settings.settings_profile_delete_confirm'))) {
this.$store.dispatch('deleteSettingsProfile', name)
}
}
}
}

View File

@ -1,7 +1,6 @@
<template>
<div :label="$t('settings.general')">
<div class="setting-item">
<h2>{{ $t('settings.interface') }}</h2>
<ul class="setting-list">
<li>
<interface-language-switcher
@ -10,6 +9,94 @@
:set-language="val => language = val"
/>
</li>
<li
v-if="user && (settingsProfiles.length > 0)"
>
<h2>{{ $t('settings.settings_profile') }}</h2>
<p>
{{ $t('settings.settings_profile_currently', { name: settingsProfile, version: settingsVersion }) }}
<button
class="btn button-default"
@click="forceSync()"
>
{{ $t('settings.settings_profile_force_sync') }}
</button>
</p>
<div
@click="toggleExpandedSettings"
>
<template
v-if="profilesExpanded"
>
<button class="btn button-default">
{{ $t('settings.settings_profiles_unshow') }}
</button>
</template>
<template
v-else
>
<button class="btn button-default">
{{ $t('settings.settings_profiles_show') }}
</button>
</template>
</div>
<br>
<template
v-if="profilesExpanded"
>
<div
v-for="profile in settingsProfiles"
:key="profile.id"
class="settings-profile"
>
<h4>{{ profile.name }} ({{ profile.version }})</h4>
<template
v-if="settingsProfile === profile.name"
>
{{ $t('settings.settings_profile_in_use') }}
</template>
<template
v-else
>
<button
class="btn button-default"
@click="loadSettingsProfile(profile.name)"
>
{{ $t('settings.settings_profile_use') }}
</button>
<button
class="btn button-default"
@click="deleteSettingsProfile(profile.name)"
>
{{ $t('settings.settings_profile_delete') }}
</button>
</template>
</div>
<button class="btn button-default" @click="refreshProfiles()">
{{ $t('settings.settings_profiles_refresh') }}
<FAIcon icon="sync" @click="refreshProfiles()" />
</button>
<h3>{{ $t('settings.settings_profile_creation') }}</h3>
<label for="settings-profile-new-name">
{{ $t('settings.settings_profile_creation_new_name_label') }}
</label>
<input v-model="newProfileName" id="settings-profile-new-name">
<button
class="btn button-default"
@click="createSettingsProfile"
>
{{ $t('settings.settings_profile_creation_submit') }}
</button>
</template>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.interface') }}</h2>
<ul class="setting-list">
<li v-if="instanceSpecificPanelPresent">
<BooleanSetting path="hideISP">
{{ $t('settings.hide_isp') }}
@ -450,7 +537,6 @@
{{ $t('settings.default_vis') }} <ServerSideIndicator :server-side="true" />
<ScopeSelector
class="scope-selector"
:show-all="true"
:user-default="serverSide_defaultScope"
:initial-scope="serverSide_defaultScope"
:on-scope-change="changeDefaultScope"
@ -458,12 +544,6 @@
</label>
</li>
<li>
<BooleanSetting path="minimalScopesMode">
{{ $t('settings.minimal_scopes_mode') }}
</BooleanSetting>
</li>
<li>
<!-- <BooleanSetting path="serverSide_defaultNSFW"> -->
<BooleanSetting path="sensitiveByDefault">
{{ $t('settings.sensitive_by_default') }}
</BooleanSetting>
@ -473,14 +553,6 @@
{{ $t('settings.sensitive_if_subject') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="scopeCopy"
expert="1"
>
{{ $t('settings.scope_copy') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="alwaysShowSubjectInput"
@ -508,14 +580,6 @@
{{ $t('settings.post_status_content_type') }}
</ChoiceSetting>
</li>
<li>
<BooleanSetting
path="minimalScopesMode"
expert="1"
>
{{ $t('settings.minimal_scopes_mode') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="alwaysShowNewPostButton"
@ -546,3 +610,13 @@
</template>
<script src="./general_tab.js"></script>
<style lang="scss">
.settings-profile {
margin-bottom: 1em;
}
#settings-profile-new-name {
margin-left: 1em;
margin-right: 1em;
}
</style>

View File

@ -43,7 +43,9 @@ const ProfileTab = {
bannerPreview: null,
background: null,
backgroundPreview: null,
emailLanguage: this.$store.state.users.currentUser.language || ''
emailLanguage: this.$store.state.users.currentUser.language || '',
newPostTTLDays: this.$store.state.users.currentUser.status_ttl_days,
expirePosts: this.$store.state.users.currentUser.status_ttl_days !== null,
}
},
components: {
@ -123,7 +125,8 @@ const ProfileTab = {
display_name: this.newName,
fields_attributes: this.newFields.filter(el => el != null),
bot: this.bot,
show_role: this.showRole
show_role: this.showRole,
status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1
/* eslint-enable camelcase */
}
@ -151,7 +154,7 @@ const ProfileTab = {
return false
},
deleteField (index, event) {
this.$delete(this.newFields, index)
this.newFields.splice(index, 1)
},
uploadFile (slot, e) {
const file = e.target.files[0]

View File

@ -4,6 +4,10 @@
margin: 0;
}
.expire-posts-days {
margin-left: 1em;
}
.visibility-tray {
padding-top: 5px;
}

View File

@ -89,6 +89,20 @@
{{ $t('settings.bot') }}
</Checkbox>
</p>
<p>
<Checkbox v-model="expirePosts">
{{ $t('settings.expire_posts_enabled') }}
</Checkbox>
<input
v-model="newPostTTLDays"
:disabled="!expirePosts"
type="number"
min="1"
max="730"
class="expire-posts-days"
:placeholder="$t('settings.expire_posts_input_placeholder')"
/>
</p>
<p>
<interface-language-switcher
:prompt-text="$t('settings.email_language')"

View File

@ -753,7 +753,6 @@ export default {
selected () {
this.selectedTheme = Object.entries(this.availableStyles).find(([k, s]) => {
if (Array.isArray(s)) {
console.log(s[0] === this.selected, this.selected)
return s[0] === this.selected
} else {
return s.name === this.selected

View File

@ -284,7 +284,6 @@
box-shadow: none;
background: transparent;
color: var(--faint, $fallback--faint);
align-self: stretch;
}
.theme-color-cl,
@ -318,11 +317,11 @@
.extra-content {
.apply-container {
padding-left: 15vw;
display: flex;
flex-direction: row;
justify-content: space-around;
justify-content: space-evenly;
flex-grow: 1;
.btn {
flex-grow: 1;
min-height: 2em;

View File

@ -958,20 +958,22 @@
v-if="isActive"
to="#unscrolled-content"
>
<div class="apply-container">
<button
class="btn button-default submit"
:disabled="!themeValid"
@click="setCustomTheme"
>
{{ $t('general.apply') }}
</button>
<button
class="btn button-default"
@click="clearAll"
>
{{ $t('settings.style.switcher.reset') }}
</button>
<div class="panel-body settings-footer">
<div class="apply-container">
<button
class="btn button-default submit"
:disabled="!themeValid"
@click="setCustomTheme"
>
{{ $t('general.apply') }}
</button>
<button
class="btn button-default"
@click="clearAll"
>
{{ $t('settings.style.switcher.reset') }}
</button>
</div>
</div>
</teleport>
</div>

View File

@ -15,7 +15,8 @@ import {
faTachometerAlt,
faCog,
faInfoCircle,
faList
faList,
faUserTie
} from '@fortawesome/free-solid-svg-icons'
library.add(
@ -30,7 +31,8 @@ library.add(
faTachometerAlt,
faCog,
faInfoCircle,
faList
faList,
faUserTie
)
const SideDrawer = {
@ -102,6 +104,9 @@ const SideDrawer = {
},
openSettingsModal () {
this.$store.dispatch('openSettingsModal')
},
openModModal () {
this.$store.dispatch('openModModal')
}
}
}

View File

@ -143,6 +143,21 @@
/> {{ $t("nav.about") }}
</router-link>
</li>
<li
v-if="currentUser && currentUser.role === 'admin' || currentUser.role === 'moderator'"
@click="toggleDrawer"
>
<button
class="button-unstyled -link -fullwidth"
@click="openModModal"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="user-tie"
/> {{ $t("nav.moderation") }}
</button>
</li>
<li
v-if="currentUser && currentUser.role === 'admin'"
@click="toggleDrawer"

View File

@ -460,6 +460,16 @@ const Status = {
return 'globe'
}
},
faviconAlt (status) {
if (!status.user.instance) {
return ''
}
const software = ((status.user.instance) && (status.user.instance.nodeinfo) && (status.user.instance.nodeinfo.software)) || {}
if (software.name) {
return `${status.user.instance.name} (${software.name || ''} ${software.version || ''})`
}
return ''
},
showError (error) {
this.error = error
},

View File

@ -1,4 +1,4 @@
@import '../../_variables.scss';
@import "../../_variables.scss";
.Status {
min-width: 0;
@ -42,6 +42,10 @@
display: flex;
padding: var(--status-margin, $status-margin);
.content {
overflow: hidden;
}
> * {
min-width: 0;
}
@ -130,6 +134,15 @@
.heading-left {
display: flex;
min-width: 0;
flex-wrap: wrap;
img {
aspect-ratio: 1 / 1;
}
.nowrap {
white-space: nowrap;
}
}
.heading-right {
@ -139,6 +152,7 @@
.button-unstyled {
padding: 5px;
margin: -5px;
height: min-content;
&:hover svg {
color: $fallback--lightText;
@ -185,7 +199,7 @@
.reply-to-popover {
.reply-to:hover::before {
content: '';
content: "";
display: block;
position: absolute;
bottom: 0;
@ -195,13 +209,12 @@
}
.faint-link:hover {
// override default
text-decoration: none;
}
&.-strikethrough {
.reply-to::after {
content: '';
content: "";
display: block;
position: absolute;
top: 50%;
@ -293,10 +306,12 @@
position: relative;
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: left;
margin-top: var(--status-margin, $status-margin);
> * {
max-width: 4em;
min-width: fit-content;
flex: 1;
}
}
@ -340,7 +355,7 @@
margin-left: 0.2em;
&::before {
content: ' ';
content: " ";
}
}
@ -387,7 +402,7 @@
align-items: center;
&::before {
content: '';
content: "";
position: absolute;
height: 100%;
width: 1px;

View File

@ -166,18 +166,21 @@
>
{{ status.user.name }}
</h4>
<router-link
class="account-name"
:title="status.user.screen_name_ui"
:to="userProfileLink"
>
{{ status.user.screen_name_ui }}
</router-link>
<img
v-if="!!(status.user && status.user.favicon)"
class="status-favicon"
:src="status.user.favicon"
>
<span class="nowrap">
<router-link
class="account-name"
:title="status.user.screen_name_ui"
:to="userProfileLink"
>
@{{ status.user.screen_name_ui }}
</router-link>
<img
v-if="!!(status.user && status.user.favicon)"
class="status-favicon"
:src="status.user.favicon"
:title="faviconAlt(status)"
>
</span>
</div>
<span class="heading-right">
@ -349,22 +352,25 @@
</div>
</div>
<StatusContent
ref="content"
:status="status"
:no-heading="noHeading"
:highlight="highlight"
:focused="isFocused"
:controlled-showing-tall="controlledShowingTall"
:controlled-expanding-subject="controlledExpandingSubject"
:controlled-showing-long-subject="controlledShowingLongSubject"
:controlled-toggle-showing-tall="controlledToggleShowingTall"
:controlled-toggle-expanding-subject="controlledToggleExpandingSubject"
:controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject"
@mediaplay="addMediaPlaying($event)"
@mediapause="removeMediaPlaying($event)"
@parseReady="setHeadTailLinks"
/>
<div class="content">
<StatusContent
ref="content"
class="status-content"
:status="status"
:no-heading="noHeading"
:highlight="highlight"
:focused="isFocused"
:controlled-showing-tall="controlledShowingTall"
:controlled-expanding-subject="controlledExpandingSubject"
:controlled-showing-long-subject="controlledShowingLongSubject"
:controlled-toggle-showing-tall="controlledToggleShowingTall"
:controlled-toggle-expanding-subject="controlledToggleExpandingSubject"
:controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject"
@mediaplay="addMediaPlaying($event)"
@mediapause="removeMediaPlaying($event)"
@parseReady="setHeadTailLinks"
/>
</div>
<div
v-if="inConversation && !isPreview && replies && replies.length"
@ -531,6 +537,6 @@
</div>
</template>
<script src="./status.js" ></script>
<script src="./status.js"></script>
<style src="./status.scss" lang="scss"></style>

View File

@ -83,7 +83,7 @@ const StatusContent = {
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
},
translationLanguages () {
return (this.$store.getters.mergedConfig.supportedTranslationLanguages.source || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
return (this.$store.state.instance.supportedTranslationLanguages.source || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
},
...mapGetters(['mergedConfig'])
},

View File

@ -1,7 +1,7 @@
<template>
<div
class="StatusBody"
:class="{ '-compact': compact }"
:class="{ '-compact': compact, 'mfm-disabled': !renderMisskeyMarkdown }"
>
<div class="body">
<div

View File

@ -82,9 +82,16 @@
}
}
&.mfm-disabled {
span {
font-size: 100% !important;
}
.mfm {
animation: none !important;
}
.emoji {
width: 32px !important;
height: 32px !important;
}
}
}

View File

@ -11,12 +11,13 @@ const StillImage = {
],
data () {
return {
stopGifs: this.$store.getters.mergedConfig.stopGifs
stopGifs: this.$store.getters.mergedConfig.stopGifs,
isAnimated: false
}
},
computed: {
animated () {
return this.stopGifs && (this.mimetype === 'image/gif' || this.src.endsWith('.gif'))
return this.stopGifs && this.isAnimated
},
style () {
const appendPx = (str) => /\d$/.test(str) ? str + 'px' : str
@ -31,17 +32,89 @@ const StillImage = {
const image = this.$refs.src
if (!image) return
this.imageLoadHandler && this.imageLoadHandler(image)
this.detectAnimation(image)
this.drawThumbnail()
},
onError () {
this.imageLoadError && this.imageLoadError()
},
detectAnimation (image) {
if (this.mimetype === 'image/gif' || this.src.endsWith('.gif')) {
this.isAnimated = true
return
}
// harmless CORS errors without-- clean console with
if (!this.$store.state.instance.mediaProxyAvailable) return
// Animated JPEGs?
if (!(this.src.endsWith('.webp') || this.src.endsWith('.png'))) return
// Browser Cache should ensure image doesn't get loaded twice if cache exists
fetch(image.src, {
referrerPolicy: 'same-origin'
})
.then(data => {
// We don't need to read the whole file so only call it once
data.body.getReader().read()
.then(reader => {
if (this.src.endsWith('.webp') && this.isAnimatedWEBP(reader.value)) {
this.isAnimated = true
return
}
if (this.src.endsWith('.png') && this.isAnimatedPNG(reader.value)) {
this.isAnimated = true
}
})
})
.catch(() => {
// this.imageLoadError && this.imageLoadError()
})
},
isAnimatedWEBP (data) {
/**
* WEBP HEADER CHUNK
* === START HEADER ===
* 82 73 70 70 ("RIFF")
* xx xx xx xx (SIZE)
* 87 69 66 80 ("WEBP")
* === END OF HEADER ===
* 86 80 56 88 ("VP8X") Extended VP8X
* xx xx xx xx (VP8X)
* [++] RSVILEX(A)R (1 byte)
* A Animated bit
*/
// Relevant bytes
const segment = data.slice(4 * 3, (4 * 5) + 1)
// Check for VP8X string
if (segment.join('').includes(['86805688'])) {
// Check for Animation bit
return !!((segment[8] >> 1) & 1)
}
// No VP8X = Not Animated (X is for Extended)
return false
},
isAnimatedPNG (data) {
// Find acTL before IDAT in PNG; if found it is animated
const segment = []
for (let i = 0; i < data.length; i++) {
segment.push(String.fromCharCode(data[i]))
}
const str = segment.join('')
const idatPos = str.indexOf('IDAT')
return (str.substring(0, idatPos > 0 ? idatPos : 0).indexOf('acTL') > 0)
},
drawThumbnail () {
const canvas = this.$refs.canvas
if (!canvas) return
if (!this.$refs.canvas) return
const image = this.$refs.src
const width = image.naturalWidth
const height = image.naturalHeight
canvas.width = width
canvas.height = height
canvas.getContext('2d').drawImage(image, 0, 0, width, height)
},
onError () {
this.imageLoadError && this.imageLoadError()
}
},
updated () {
// On computed animated change
this.drawThumbnail()
}
}

View File

@ -64,8 +64,12 @@ export default {
settingsModalVisible () {
return this.settingsModalState === 'visible'
},
modModalVisible () {
return this.modModalState === 'visible'
},
...mapState({
settingsModalState: state => state.interface.settingsModalState
settingsModalState: state => state.interface.settingsModalState,
modModalState: state => state.interface.modModalState
})
},
beforeUpdate () {

View File

@ -6,11 +6,13 @@ import TimelineMenuTabs from '../timeline_menu_tabs/timeline_menu_tabs.vue'
import TimelineQuickSettings from './timeline_quick_settings.vue'
import { debounce, throttle, keyBy } from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch, faCog } from '@fortawesome/free-solid-svg-icons'
import { faCircleNotch, faCog, faPlus, faMinus } from '@fortawesome/free-solid-svg-icons'
library.add(
faCircleNotch,
faCog
faCog,
faPlus,
faMinus
)
const Timeline = {
@ -90,6 +92,15 @@ const Timeline = {
},
showPanelNavShortcuts () {
return this.$store.getters.mergedConfig.showPanelNavShortcuts
},
currentUser () {
return this.$store.state.users.currentUser
},
tagData () {
return this.$store.state.tags.tags[this.tag]
},
tagFollowed () {
return this.$store.state.tags.tags[this.tag]?.following
}
},
created () {
@ -118,6 +129,10 @@ const Timeline = {
}
window.addEventListener('keydown', this.handleShortKey)
setTimeout(this.determineVisibleStatuses, 250)
if (this.tag) {
this.$store.dispatch('getTag', this.tag)
}
},
unmounted () {
window.removeEventListener('scroll', this.handleScroll)
@ -126,7 +141,7 @@ const Timeline = {
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
},
methods: {
stopBlockingClicks: debounce(function () {
stopBlockingClicks: debounce( function() {
this.blockingClicks = false
}, 1000),
blockClicksTemporarily () {
@ -154,7 +169,7 @@ const Timeline = {
window.scrollTo({ top: 0 })
}
},
fetchOlderStatuses: throttle(function () {
fetchOlderStatuses: throttle( function () {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
store.commit('setLoading', { timeline: this.timelineName, value: true })
@ -188,7 +203,7 @@ const Timeline = {
const centerOfScreen = window.pageYOffset + (window.innerHeight * 0.5)
// Start from approximating the index of some visible status by using the
// Start from approximating the index of some visible status by using
// the center of the screen on the timeline.
let approxIndex = Math.floor(statuses.length * (centerOfScreen / height))
let err = statuses[approxIndex].getBoundingClientRect().y
@ -226,12 +241,18 @@ const Timeline = {
this.fetchOlderStatuses()
}
},
handleScroll: throttle(function (e) {
handleScroll: throttle( function (e) {
this.determineVisibleStatuses()
this.scrollLoad(e)
}, 200),
handleVisibilityChange () {
this.unfocused = document.hidden
},
followTag (tag) {
return this.$store.dispatch('followTag', tag)
},
unfollowTag (tag) {
return this.$store.dispatch('unfollowTag', tag)
}
},
watch: {

View File

@ -21,6 +21,36 @@
{{ $t('timeline.up_to_date') }}
</div>
<TimelineQuickSettings v-if="!embedded" />
<div
v-if="currentUser && tag !== undefined && tagData && !tagFollowed"
class="followTag"
>
<button
class="button-default"
:title="$t('timeline.follow_tag')"
@click="followTag(tag)"
>
<FAIcon
size="sm"
icon="plus"
/>
</button>
</div>
<div
v-if="currentUser && tag !== undefined && tagData && tagFollowed"
class="followTag"
>
<button
class="button-default"
:title="$t('timeline.unfollow_tag')"
@click="unfollowTag(tag)"
>
<FAIcon
size="sm"
icon="minus"
/>
</button>
</div>
</div>
<div :class="classes.body">
<div

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