Compare commits

...

156 commits

Author SHA1 Message Date
13f92fa2b1 format missed files 2023-07-05 02:35:36 -04:00
8e880c349e update ciu-lite 2023-07-05 02:34:03 -04:00
3ca4c32b03 format 2023-07-05 02:33:10 -04:00
d25dd1cbd4 Merge remote-tracking branch 'upstream/develop' into develop 2023-07-05 02:29:15 -04:00
42ffce97d6 Merge remote-tracking branch 'origin/translations' into dm-privacy 2023-05-23 13:47:14 +01:00
2f479c670f Add DM settings 2023-05-23 13:46:59 +01:00
Weblate
ee6e7026ab Merge branch 'origin/develop' into Weblate. 2023-05-23 11:38:58 +00:00
17c05a5ca2 Merge pull request 'paper theme: more contrast and fix setting tab hover' (#314) from denys/akkoma-fe:cool-paper-theme into develop
Reviewed-on: AkkomaGang/akkoma-fe#314
2023-05-23 11:38:57 +00:00
Weblate
42896c2abf Merge branch 'origin/develop' into Weblate. 2023-05-23 11:38:30 +00:00
ecb6be2152 Merge pull request 'fix unfinished post being sent when scrolling' (#312) from denys/akkoma-fe:accidental-mobile-posts into develop
Reviewed-on: AkkomaGang/akkoma-fe#312
2023-05-23 11:38:28 +00:00
Weblate
6c92983af6 Merge branch 'origin/develop' into Weblate. 2023-05-23 11:37:26 +00:00
9e4985e225 Merge pull request 'fix apply theme button without page refresh' (#309) from denys/akkoma-fe:fix-apply-theme into develop
Reviewed-on: AkkomaGang/akkoma-fe#309
2023-05-23 11:37:24 +00:00
Weblate
60ff715aff Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1042 of 1042 strings)

Co-authored-by: Poesty Li <poesty7450@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/zh_Hans/
Translation: Pleroma fe/pleroma-fe
2023-05-21 20:58:06 +00:00
Weblate
04bcf7d804 Translated using Weblate (Polish)
Currently translated at 66.1% (689 of 1042 strings)

Co-authored-by: Jeder <jeder@jeder.pl>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/pl/
Translation: Pleroma fe/pleroma-fe
2023-05-21 20:58:06 +00:00
Weblate
5fa305c58c Translated using Weblate (Japanese (ja_EASY))
Currently translated at 72.0% (751 of 1042 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: kazari <6c577a54-aac9-482a-955e-745c858445e3@simplelogin.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ja_EASY/
Translation: Pleroma fe/pleroma-fe
2023-05-21 20:58:06 +00:00
Weblate
a2ceb89d5e Translated using Weblate (Turkish)
Currently translated at 4.0% (42 of 1042 strings)

Added translation using Weblate (Turkish)

Co-authored-by: Hasan Yıldız <hasanyildiz0@yaani.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/tr/
Translation: Pleroma fe/pleroma-fe
2023-05-21 20:58:06 +00:00
6b3b55455d paper theme: more contrast and fix setting tab hover 2023-05-18 23:05:19 +03:00
8c6ccc321d fix unfinished post being sent when scrolling 2023-05-15 03:11:07 +03:00
596ae7e377 Merge pull request 'fix dropdown-item-icon and form controls using missing variables' (#307) from denys/akkoma-fe:missing-sass-vars into develop
Reviewed-on: AkkomaGang/akkoma-fe#307
2023-05-08 15:29:58 +00:00
0d22a22a10 Merge pull request 'order bubble after public in sidebar like in other two menus' (#306) from denys/akkoma-fe:consistent-bubble-order into develop
Reviewed-on: AkkomaGang/akkoma-fe#306
2023-05-08 15:28:54 +00:00
2a76be56e7 fix apply theme button without page refresh 2023-05-01 20:54:18 +03:00
661a98d38d order bubble after public in sidebar like in other two menus 2023-05-01 20:53:29 +03:00
94d640f9f1 fix dropdown-item-icon and form controls using missing variables 2023-05-01 20:50:31 +03:00
Weblate
1f943ce8a5 Merge branch 'origin/develop' into Weblate. 2023-04-14 16:43:42 +00:00
c540764408 ensure we only fetch reports when we're an admin
Ref #288
2023-04-14 17:43:05 +01:00
Weblate
a4dfdc0853 Merge branch 'origin/develop' into Weblate. 2023-04-14 16:30:56 +00:00
ddea499a36 Merge pull request 'Fix edits and redrafts being erased by drafts' (#297) from solidsanek/pleroma-fe:drafts-edit-redraft-fix into develop
Reviewed-on: AkkomaGang/akkoma-fe#297
2023-04-14 16:30:55 +00:00
solidsanek
db33fe8ee2 Drafts: Fix drafts erasing edits and redrafts 2023-04-09 11:02:13 +02:00
Weblate
f1bf22436d Translated using Weblate (Portuguese)
Currently translated at 62.8% (655 of 1042 strings)

Co-authored-by: cel <8cbv6di5@duck.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/pt/
Translation: Pleroma fe/pleroma-fe
2023-04-05 18:57:45 +00:00
Weblate
459c73ec02 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1041 of 1041 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.9% (1040 of 1041 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.9% (1040 of 1041 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.9% (1039 of 1040 strings)

Co-authored-by: Poesty Li <poesty7450@gmail.com>
Co-authored-by: SevicheCC <sevicheee@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/zh_Hans/
Translation: Pleroma fe/pleroma-fe
2023-04-05 18:57:45 +00:00
Weblate
2acf1e5c59 Translated using Weblate (Ukrainian)
Currently translated at 87.2% (908 of 1041 strings)

Co-authored-by: Denys Nykula <vegan@libre.net.ua>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/uk/
Translation: Pleroma fe/pleroma-fe
2023-04-05 18:57:45 +00:00
Weblate
33c4459744 Translated using Weblate (French)
Currently translated at 98.5% (1027 of 1042 strings)

Translated using Weblate (French)

Currently translated at 98.3% (1024 of 1041 strings)

Translated using Weblate (French)

Currently translated at 96.9% (1002 of 1033 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
2023-04-05 18:57:45 +00:00
Weblate
b00487e51f Translated using Weblate (Japanese (ja_EASY))
Currently translated at 71.6% (747 of 1042 strings)

Translated using Weblate (Japanese (ja_EASY))

Currently translated at 71.6% (747 of 1042 strings)

Translated using Weblate (Japanese (ja_EASY))

Currently translated at 54.1% (564 of 1042 strings)

Co-authored-by: Hikaru Shinagawa <hikali.47041@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: kazari <6c577a54-aac9-482a-955e-745c858445e3@simplelogin.com>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ja_EASY/
Translation: Pleroma fe/pleroma-fe
2023-04-05 18:57:45 +00:00
Weblate
1e1cab643c Translated using Weblate (Dutch)
Currently translated at 99.7% (1038 of 1041 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
2023-04-05 18:57:45 +00:00
Weblate
8d3219a6d2 Translated using Weblate (Indonesian)
Currently translated at 65.4% (676 of 1033 strings)

Translated using Weblate (Indonesian)

Currently translated at 65.4% (676 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
2023-04-05 18:57:45 +00:00
Weblate
ec9753758f Translated using Weblate (Spanish)
Currently translated at 90.8% (938 of 1033 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
2023-04-05 18:57:45 +00:00
Weblate
97ff4a7241 Translated using Weblate (German)
Currently translated at 99.5% (1036 of 1041 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
2023-04-05 18:57:45 +00:00
14cedc5ed1 don't crash if class isn't a list 2023-04-01 07:55:47 +01:00
5911777aa2 Merge pull request 'Fix floating point error for poll expiry' (#294) from xarvos/pleroma-fe:fix-poll-expire into develop
Reviewed-on: AkkomaGang/akkoma-fe#294
2023-03-30 09:49:38 +00:00
47fc082fb9
Fix floating point error for poll expiry
Previous code multiply with 0.001 before multiplication which leads to a
floating point error.  By changing it to division by 1000 after
multiplication this is avoided.
2023-03-24 20:48:02 +07:00
7e1b1e79f4 simplify timeline vibility checks 2023-03-18 20:22:47 +00:00
b92b2f74a4 add timeline visibility setting parsing 2023-03-18 20:01:05 +00:00
7361f4e77e Add checks for currentUser on sidebar 2023-03-16 16:41:37 +00:00
9f7f9e2798 Remove unused bits and bobs 2023-03-15 23:00:31 +00:00
42ab3eada4 Remove links from navs if we can't see the timeline 2023-03-15 22:20:54 +00:00
6fdef479d0 add recently used emojis panel to emoji picker (#283)
~~(not intended for merging yet, just submitting this for preliminary review and discussion)~~

this patch adds a tab with recently used emojis to the emoji picker: https://akko.lain.gay/notice/ASoGCtyoiXbYPJjqpk

there's a couple of things i'm ~~still trying to work out~~ not totally happy with and i'd appreciate any feedback on them:

* the recentEmojis getter is called very frequently and has to do a possibly somewhat expensive lookup of emoji objects by their `displayName` each time, which i'm not sure is ideal
* ~~emoji reactions on posts added through the picker are picked up by the recentEmojis module, but clicks on existing emoji reactions are not, because `addReaction` in `react_button.js` only currently receives the replacement and not the full emoji object (if there even is one wherever that method is called from)~~ this works now and does the same stupid full search of all emojis by their name which i guess is less bad because this only happens when you hit a reaction emoji button that already existed

Reviewed-on: AkkomaGang/akkoma-fe#283
Co-authored-by: flisk <akkomadev.mvch71fq@flisk.xyz>
Co-committed-by: flisk <akkomadev.mvch71fq@flisk.xyz>
2023-03-10 19:10:42 +00:00
fe08691f05 Merge pull request 'support Misskey's oblong reactions' (#284) from yheuhtozr/pleroma-fe:oblong-reactions into develop
Reviewed-on: AkkomaGang/akkoma-fe#284
2023-03-10 18:57:38 +00:00
6a9764951f Merge pull request 'fix realtime updates in 'following' replies filter' (#285) from flisk/akkoma-fe:fix-realtime-reply-filter into develop
Reviewed-on: AkkomaGang/akkoma-fe#285
2023-03-10 18:56:31 +00:00
0f33b1cd79 Merge pull request 'Post drafting' (#282) from solidsanek/pleroma-fe:drafts into develop
Reviewed-on: AkkomaGang/akkoma-fe#282
2023-03-10 18:55:03 +00:00
999c38594e fix realtime updates in 'following' replies filter
i'm not sure how this code was supposed to work, but the way it was
written would only add statuses to the timeline if they were in reply to
someone the user is following and erroneously filter out posts that
aren't replies.
2023-02-24 00:23:53 +01:00
626c880038 oblong emoji in status 2023-02-22 10:20:25 +09:00
6d7761c7e5 perhaps more graceful cqw 2023-02-20 23:27:41 +09:00
996ce3dde3 support oblong reactions 2023-02-20 23:18:04 +09:00
solidsanek
2c007f06e3 Post: remove debug logs 2023-02-19 18:58:53 +01:00
solidsanek
00704bd88c Post: Add drafting feature 2023-02-17 13:56:01 +01:00
3cee6c5934 more formatting 2023-02-15 00:12:10 +00:00
5476a2794d fix missed conflicts 2023-02-15 00:03:00 +00:00
d8fa8c4ee4 Merge remote-tracking branch 'upstream/develop' into develop 2023-02-14 23:54:04 +00:00
6a9d169e24 Merge pull request 'components: emoji_reactions: force custom emoji reaction height' (#280) from a1batross/akkoma-fe:a1batross-patch-1 into develop
Reviewed-on: AkkomaGang/akkoma-fe#280
2023-02-11 10:41:00 +00:00
581c53a15e components: emoji_reactions: force custom emoji reaction height
Prevents the usage of too long emoji reactions
2023-02-10 23:28:46 +00:00
9e04e4fd80 Improve emoji picker performance (#275)
A simple virtual scroller is now used for the emoji grid. This avoids loading all emoji images at once, saving network bandwidth and reducing load on the server, while also putting less work on the browser's DOM and layout engine.

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

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

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

Closes: AkkomaGang/pleroma-fe#255

Co-authored-by: Francis Dinh <normandy@biribiri.dev>
Reviewed-on: AkkomaGang/akkoma-fe#256
Co-authored-by: Norm <normandy@biribiri.dev>
Co-committed-by: Norm <normandy@biribiri.dev>
2022-12-30 03:01:17 +00:00
dd403b295f Merge pull request 'Remove stray debug log' (#265) from sfr/pleroma-fe:del-log into develop
Reviewed-on: AkkomaGang/akkoma-fe#265
2022-12-30 03:00:49 +00:00
Sol Fisher Romanoff
9cd62fe08d
Remove stray debug log 2022-12-30 01:03:31 +02:00
f668455dff Merge branch 'link-verification' into develop 2022-12-29 20:56:22 +00:00
5a4315384e force CI build 2022-12-29 15:25:03 +00:00
401dfa8fa6 update readme 2022-12-29 15:22:06 +00:00
bb243168b3 Revert "Merge pull request 'Don't show timeline links if disabled and logged out' (#250) from sfr/pleroma-fe:fix/hide-timelines into develop"
This reverts commit 0b5793c1e0, reversing
changes made to 72ef2e7454.
2022-12-29 15:18:13 +00:00
da491f3278 add verification of links 2022-12-29 15:17:35 +00:00
d00e28d5e9 fix emoji picker in replies in notifications 2022-12-22 05:43:01 +00:00
7ff17ab722 don't crash out if notification status is null 2022-12-20 13:20:13 +00:00
b009428814 Merge pull request 'Revert "Revert "use v1 urls""' (#254) from v1-urls into develop
Reviewed-on: AkkomaGang/pleroma-fe#254
2022-12-14 12:09:03 +00:00
7bec96a1bf Merge pull request 'Fix user moderation dropdown clipping' (#249) from sfr/pleroma-fe:fix/dropdown into develop
Reviewed-on: AkkomaGang/pleroma-fe#249
2022-12-14 12:08:27 +00:00
0b5793c1e0 Merge pull request 'Don't show timeline links if disabled and logged out' (#250) from sfr/pleroma-fe:fix/hide-timelines into develop
Reviewed-on: AkkomaGang/pleroma-fe#250
2022-12-14 12:08:08 +00:00
72ef2e7454 Merge pull request 'Fix 404 when reacting with Keycap Number Sign' (#252) from fef/pleroma-fe:develop into develop
Reviewed-on: AkkomaGang/pleroma-fe#252
2022-12-14 12:07:27 +00:00
c39332c1bf Revert "Revert "use v1 urls""
This reverts commit 8c6cf86de3.
2022-12-14 09:39:01 +00:00
8c6cf86de3 Revert "use v1 urls"
This reverts commit 909271c764.
2022-12-14 09:38:46 +00:00
909271c764 use v1 urls 2022-12-14 09:38:07 +00:00
fb317f2907 fix accidentally removed borders in timeline views 2022-12-14 02:39:20 +00:00
153c4d251f update checked state as well 2022-12-14 02:29:40 +00:00
1d01475f7a fix input and checkbox styling issue 2022-12-14 02:27:28 +00:00
a91e8d282d use svg as favicon 2022-12-14 02:18:20 +00:00
fef
413acbc7dd
fix 404 when reacting with Keycap Number Sign
The Unicode sequence for the Keycap Number Sign
emoji starts with an ASCII "#" character, which
the browser's URL parser will interpret as a URI
fragment and truncate it before sending the
request to the backend.
2022-12-12 18:59:57 +01:00
1312b07e2e [wip] hide protocols in links from masto servers 2022-12-11 06:47:53 +00:00
427e63cfc3 more minor padding, spacing, and link styling tweaks 2022-12-11 06:47:38 +00: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
9bf310d509 bump version 2022-12-10 14:51:08 +00:00
e3e8b19df3 fix ES translation having weird o in a key 2022-12-10 00:17:33 +00:00
9b75ca414f Merge pull request 'Visual updates pt 1' (#1) from visual-updates into develop
Reviewed-on: emma/pleroma-fe#1
2022-12-08 19:48:44 +00:00
b07cf33a04 format (almost) everything remaining with prettier 2022-12-08 19:48:04 +00:00
142f90c4cf add twitter theme preset 2022-12-08 19:36:27 +00:00
83c6f7f9f9 use system mono in mfa backup code view 2022-12-08 19:25:12 +00:00
65adfb01c3 add some nicer easings to favourite and rt button animations 2022-12-08 19:24:59 +00:00
65511042e3 make left side of conversations clickable, à la birdsite 2022-12-08 19:24:22 +00:00
235f3b2d94 allow popovers to escape the bounds of content 2022-12-08 19:23:46 +00:00
2382696698 remove gradient mask on user card 2022-12-08 19:23:05 +00:00
ae2d72131b add consistent shadows to popover elements 2022-12-08 19:22:32 +00:00
98d38e3b73 make emoji reaction buttons smaller 2022-12-08 19:21:35 +00:00
47c05363f8 make tab switcher on profile look less messy with large border radii 2022-12-08 19:20:51 +00:00
87d9c1ae15 fix various spacing issues 2022-12-08 19:20:09 +00:00
5ad0da1766 update text color on nav to be less glaring 2022-12-08 19:18:13 +00:00
97e9b2597a enable translucency on top bar & titles 2022-12-08 19:16:40 +00:00
94bbf8f0a3 update unread notification overlay look 2022-12-08 19:12:58 +00:00
ce9d316a51 use system fonts by default 2022-12-08 19:11:38 +00:00
6ce12fc153 update eslint & postcss 2022-12-08 19:08:18 +00:00
Weblate
e86c7abb39 Merge branch 'origin/develop' into Weblate. 2022-12-08 18:41:37 +00:00
8a0da8861d Merge pull request 'Add YAML bug and feat templates' (#247) from sfr/pleroma-fe:issue-template into develop
Reviewed-on: AkkomaGang/pleroma-fe#247
2022-12-08 18:41:36 +00:00
Sol Fisher Romanoff
6c7e691aea
Add YAML bug and feat templates 2022-12-08 20:24:21 +02:00
6a2cdcfc15 remove custom scrollbars 2022-12-08 16:49:31 +00:00
d7688fafd3 format everything 2022-12-08 16:48:17 +00:00
3d3425eda9 add prettier for formatting 2022-12-08 16:31:17 +00:00
Weblate
b33d15a739 Merge branch 'origin/develop' into Weblate. 2022-12-07 22:37:54 +00:00
40e86998e6 Update 'ISSUE_TEMPLATE.md' 2022-12-07 22:37:52 +00:00
Weblate
177f344033 Merge branch 'origin/develop' into Weblate. 2022-12-07 22:32:07 +00:00
9079ac4afa Update 'ISSUE_TEMPLATE.md' 2022-12-07 22:31:49 +00:00
Weblate
dfc4e0a026 Translated using Weblate (Japanese (ja_PEDANTIC))
Currently translated at 99.8% (1031 of 1033 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 53.0% (548 of 1033 strings)

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

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

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

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

Co-authored-by: Charlotte 🦝 Delenk <lotte@chir.rs>
Reviewed-on: AkkomaGang/pleroma-fe#234
Co-authored-by: darkkirb <lotte@chir.rs>
Co-committed-by: darkkirb <lotte@chir.rs>
2022-11-26 20:57:04 +00:00
451 changed files with 15233 additions and 9511 deletions

View file

@ -1,5 +1,9 @@
{ {
"presets": ["@babel/preset-env"], "presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"], "plugins": [
"@babel/plugin-transform-runtime",
"lodash",
"@vue/babel-plugin-jsx"
],
"comments": false "comments": false
} }

View file

@ -5,14 +5,9 @@ module.exports = {
sourceType: 'module' sourceType: 'module'
}, },
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: [ extends: ['plugin:vue/recommended', 'plugin:prettier/recommended'],
'plugin:vue/recommended'
],
// required to lint *.vue files // required to lint *.vue files
plugins: [ plugins: ['vue', 'import'],
'vue',
'import'
],
// add your custom rules here // add your custom rules here
rules: { rules: {
// allow paren-less arrow functions // allow paren-less arrow functions

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."

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"trailingComma": "none",
"singleQuote": true,
"semi": false,
"singleAttributePerLine": true
}

View file

@ -1,22 +1,22 @@
# Pleroma-FE # Akkoma-FE
![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet) ![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet)
This is a fork of Pleroma-FE from the Pleroma project, with support for new Akkoma features such as: This is a fork of Akkoma-FE from the Pleroma project, with support for new Akkoma features such as:
- MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm) - MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm)
- Custom emoji reactions - Custom emoji reactions
# For Translators # For Translators
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Pleroma-FE. The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Akkoma-FE.
Alternatively, edit/create `src/i18n/$LANGUAGE_CODE.json` (where `$LANGUAGE_CODE` is the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language), then add your language to [src/i18n/messages.js](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/i18n/messages.js) if it doesn't already exist there. Alternatively, edit/create `src/i18n/$LANGUAGE_CODE.json` (where `$LANGUAGE_CODE` is the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language), then add your language to [src/i18n/messages.js](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/i18n/messages.js) if it doesn't already exist there.
Pleroma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js. Akkoma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js.
# FOR ADMINS # FOR ADMINS
To use Pleroma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/) CLI task to install Pleroma-FE, then modify your configuration as described in the [Frontend Management](https://docs.akkoma.dev/stable/configuration/frontend_management/) doc. To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/) CLI task to install Akkoma-FE, then modify your configuration as described in the [Frontend Management](https://docs.akkoma.dev/stable/configuration/frontend_management/) doc.
## Build Setup ## Build Setup
@ -52,4 +52,4 @@ Edit config.json for configuration.
### Login methods ### Login methods
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations. ```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.

View file

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

View file

@ -2,8 +2,7 @@ var semver = require('semver')
var chalk = require('chalk') var chalk = require('chalk')
var packageConfig = require('../package.json') var packageConfig = require('../package.json')
var exec = function (cmd) { var exec = function (cmd) {
return require('child_process') return require('child_process').execSync(cmd).toString().trim()
.execSync(cmd).toString().trim()
} }
var versionRequirements = [ var versionRequirements = [
@ -24,16 +23,23 @@ module.exports = function () {
for (var i = 0; i < versionRequirements.length; i++) { for (var i = 0; i < versionRequirements.length; i++) {
var mod = versionRequirements[i] var mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' + warnings.push(
chalk.red(mod.currentVersion) + ' should be ' + mod.name +
chalk.green(mod.versionRequirement) ': ' +
chalk.red(mod.currentVersion) +
' should be ' +
chalk.green(mod.versionRequirement)
) )
} }
} }
if (warnings.length) { if (warnings.length) {
console.log('') console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:')) console.log(
chalk.yellow(
'To use this template, you must update following to modules:'
)
)
console.log() console.log()
for (var i = 0; i < warnings.length; i++) { for (var i = 0; i < warnings.length; i++) {
var warning = warnings[i] var warning = warnings[i]

View file

@ -6,9 +6,10 @@ var express = require('express')
var webpack = require('webpack') var webpack = require('webpack')
var opn = require('opn') var opn = require('opn')
var proxyMiddleware = require('http-proxy-middleware') var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = process.env.NODE_ENV === 'testing' var webpackConfig =
? require('./webpack.prod.conf') process.env.NODE_ENV === 'testing'
: require('./webpack.dev.conf') ? require('./webpack.prod.conf')
: require('./webpack.dev.conf')
// default port where dev server listens for incoming traffic // default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port var port = process.env.PORT || config.dev.port
@ -50,7 +51,10 @@ app.use(devMiddleware)
app.use(hotMiddleware) app.use(hotMiddleware)
// serve pure static assets // serve pure static assets
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) var staticPath = path.posix.join(
config.dev.assetsPublicPath,
config.dev.assetsSubDirectory
)
app.use(staticPath, express.static('./static')) app.use(staticPath, express.static('./static'))
module.exports = app.listen(port, function (err) { module.exports = app.listen(port, function (err) {

View file

@ -4,7 +4,8 @@ var sass = require('sass')
var MiniCssExtractPlugin = require('mini-css-extract-plugin') var MiniCssExtractPlugin = require('mini-css-extract-plugin')
exports.assetsPath = function (_path) { exports.assetsPath = function (_path) {
var assetsSubDirectory = process.env.NODE_ENV === 'production' var assetsSubDirectory =
process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory ? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory : config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path) return path.posix.join(assetsSubDirectory, _path)
@ -13,7 +14,7 @@ exports.assetsPath = function (_path) {
exports.cssLoaders = function (options) { exports.cssLoaders = function (options) {
options = options || {} options = options || {}
function generateLoaders (loaders) { function generateLoaders(loaders) {
// Extract CSS when that option is specified // Extract CSS when that option is specified
// (which is the case during production build) // (which is the case during production build)
if (options.extract) { if (options.extract) {
@ -27,11 +28,11 @@ exports.cssLoaders = function (options) {
return [ return [
{ {
test: /\.(post)?css$/, test: /\.(post)?css$/,
use: generateLoaders(['css-loader', 'postcss-loader']), use: generateLoaders(['css-loader', 'postcss-loader'])
}, },
{ {
test: /\.less$/, test: /\.less$/,
use: generateLoaders(['css-loader', 'postcss-loader', 'less-loader']), use: generateLoaders(['css-loader', 'postcss-loader', 'less-loader'])
}, },
{ {
test: /\.sass$/, test: /\.sass$/,
@ -52,8 +53,8 @@ exports.cssLoaders = function (options) {
}, },
{ {
test: /\.styl(us)?$/, test: /\.styl(us)?$/,
use: generateLoaders(['css-loader', 'postcss-loader', 'stylus-loader']), use: generateLoaders(['css-loader', 'postcss-loader', 'stylus-loader'])
}, }
] ]
} }

View file

@ -7,8 +7,8 @@ var { VueLoaderPlugin } = require('vue-loader')
var env = process.env.NODE_ENV var env = process.env.NODE_ENV
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the // check env & config/index.js to decide weither to enable CSS Sourcemaps for the
// various preprocessor loaders added to vue-loader at the end of this file // various preprocessor loaders added to vue-loader at the end of this file
var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap) var cssSourceMapDev = env === 'development' && config.dev.cssSourceMap
var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap) var cssSourceMapProd = env === 'production' && config.build.productionSourceMap
var useCssSourceMap = cssSourceMapDev || cssSourceMapProd var useCssSourceMap = cssSourceMapDev || cssSourceMapProd
var now = Date.now() var now = Date.now()
@ -18,9 +18,12 @@ module.exports = {
app: './src/main.js' app: './src/main.js'
}, },
output: { output: {
hashFunction: "sha256", // Workaround for builds with OpenSSL 3. hashFunction: 'sha256', // Workaround for builds with OpenSSL 3.
path: config.build.assetsRoot, path: config.build.assetsRoot,
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath, publicPath:
process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath,
filename: '[name].js' filename: '[name].js'
}, },
optimization: { optimization: {
@ -30,17 +33,15 @@ module.exports = {
}, },
resolve: { resolve: {
extensions: ['.js', '.jsx', '.vue', '.mjs'], extensions: ['.js', '.jsx', '.vue', '.mjs'],
modules: [ modules: [path.join(__dirname, '../node_modules')],
path.join(__dirname, '../node_modules')
],
fallback: { fallback: {
"url": require.resolve("url/"), url: require.resolve('url/')
}, },
alias: { alias: {
'static': path.resolve(__dirname, '../static'), static: path.resolve(__dirname, '../static'),
'src': path.resolve(__dirname, '../src'), src: path.resolve(__dirname, '../src'),
'assets': path.resolve(__dirname, '../src/assets'), assets: path.resolve(__dirname, '../src/assets'),
'components': path.resolve(__dirname, '../src/components'), components: path.resolve(__dirname, '../src/components'),
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js' 'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
} }
}, },
@ -66,14 +67,15 @@ module.exports = {
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
type: 'javascript/auto', type: 'javascript/auto',
loader: '@intlify/vue-i18n-loader', loader: '@intlify/vue-i18n-loader',
include: [ // Use `Rule.include` to specify the files of locale messages to be pre-compiled include: [
// Use `Rule.include` to specify the files of locale messages to be pre-compiled
path.resolve(__dirname, '../src/i18n') path.resolve(__dirname, '../src/i18n')
] ]
}, },
{ {
test: /\.mjs$/, test: /\.mjs$/,
include: /node_modules/, include: /node_modules/,
type: "javascript/auto" type: 'javascript/auto'
}, },
{ {
test: /\.vue$/, test: /\.vue$/,
@ -114,10 +116,8 @@ module.exports = {
name: utils.assetsPath('fonts/[name].[hash:7].[ext]') name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
} }
} }
}, }
] ]
}, },
plugins: [ plugins: [new VueLoaderPlugin()]
new VueLoaderPlugin()
]
} }

View file

@ -7,7 +7,9 @@ var HtmlWebpackPlugin = require('html-webpack-plugin')
// add hot-reload related code to entry chunks // add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) { Object.keys(baseWebpackConfig.entry).forEach(function (name) {
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(
baseWebpackConfig.entry[name]
)
}) })
module.exports = merge(baseWebpackConfig, { module.exports = merge(baseWebpackConfig, {
@ -20,10 +22,10 @@ module.exports = merge(baseWebpackConfig, {
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': config.dev.env, 'process.env': config.dev.env,
'COMMIT_HASH': JSON.stringify('DEV'), COMMIT_HASH: JSON.stringify('DEV'),
'DEV_OVERRIDES': JSON.stringify(config.dev.settings), DEV_OVERRIDES: JSON.stringify(config.dev.settings),
'__VUE_OPTIONS_API__': true, __VUE_OPTIONS_API__: true,
'__VUE_PROD_DEVTOOLS__': false __VUE_PROD_DEVTOOLS__: false
}), }),
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),

View file

@ -2,23 +2,27 @@ var path = require('path')
var config = require('../config') var config = require('../config')
var utils = require('./utils') var utils = require('./utils')
var webpack = require('webpack') var webpack = require('webpack')
const WorkboxPlugin = require('workbox-webpack-plugin'); const WorkboxPlugin = require('workbox-webpack-plugin')
var { merge } = require('webpack-merge') var { merge } = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf') var baseWebpackConfig = require('./webpack.base.conf')
var MiniCssExtractPlugin = require('mini-css-extract-plugin') var MiniCssExtractPlugin = require('mini-css-extract-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin') var HtmlWebpackPlugin = require('html-webpack-plugin')
var env = process.env.NODE_ENV === 'testing' var env =
process.env.NODE_ENV === 'testing'
? require('../config/test.env') ? require('../config/test.env')
: config.build.env : config.build.env
let commitHash = require('child_process') let commitHash = require('child_process')
.execSync('git rev-parse --short HEAD') .execSync('git rev-parse --short HEAD')
.toString(); .toString()
var webpackConfig = merge(baseWebpackConfig, { var webpackConfig = merge(baseWebpackConfig, {
mode: 'production', mode: 'production',
module: { module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true }) rules: utils.styleLoaders({
sourceMap: config.dev.cssSourceMap,
extract: true
})
}, },
devtool: 'source-map', devtool: 'source-map',
optimization: { optimization: {
@ -36,15 +40,15 @@ var webpackConfig = merge(baseWebpackConfig, {
new WorkboxPlugin.InjectManifest({ new WorkboxPlugin.InjectManifest({
swSrc: path.join(__dirname, '..', 'src/sw.js'), swSrc: path.join(__dirname, '..', 'src/sw.js'),
swDest: 'sw-pleroma.js', swDest: 'sw-pleroma.js',
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024, maximumFileSizeToCacheInBytes: 15 * 1024 * 1024
}), }),
// http://vuejs.github.io/vue-loader/workflow/production.html // http://vuejs.github.io/vue-loader/workflow/production.html
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': env, 'process.env': env,
'COMMIT_HASH': JSON.stringify(commitHash), COMMIT_HASH: JSON.stringify(commitHash),
'DEV_OVERRIDES': JSON.stringify(undefined), DEV_OVERRIDES: JSON.stringify(undefined),
'__VUE_OPTIONS_API__': true, __VUE_OPTIONS_API__: true,
'__VUE_PROD_DEVTOOLS__': false __VUE_PROD_DEVTOOLS__: false
}), }),
// extract css into its own file // extract css into its own file
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
@ -54,9 +58,8 @@ var webpackConfig = merge(baseWebpackConfig, {
// you can customize output by editing /index.html // you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin // see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
filename: process.env.NODE_ENV === 'testing' filename:
? 'index.html' process.env.NODE_ENV === 'testing' ? 'index.html' : config.build.index,
: config.build.index,
template: 'index.html', template: 'index.html',
inject: true, inject: true,
minify: { minify: {
@ -69,7 +72,7 @@ var webpackConfig = merge(baseWebpackConfig, {
}, },
// necessary to consistently work with multiple chunks via CommonsChunkPlugin // necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'auto' chunksSortMode: 'auto'
}), })
// split vendor js into its own file // split vendor js into its own file
// extract webpack runtime and module manifest to its own file in order to // extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated // prevent vendor hash from being updated whenever app bundle is updated
@ -87,9 +90,7 @@ if (config.build.productionGzip) {
asset: '[path].gz[query]', asset: '[path].gz[query]',
algorithm: 'gzip', algorithm: 'gzip',
test: new RegExp( test: new RegExp(
'\\.(' + '\\.(' + config.build.productionGzipExtensions.join('|') + ')$'
config.build.productionGzipExtensions.join('|') +
')$'
), ),
threshold: 10240, threshold: 10240,
minRatio: 0.8 minRatio: 0.8

View file

@ -38,6 +38,11 @@ module.exports = {
assetsSubDirectory: 'static', assetsSubDirectory: 'static',
assetsPublicPath: '/', assetsPublicPath: '/',
proxyTable: { proxyTable: {
'/manifest.json': {
target,
changeOrigin: true,
cookieDomainRewrite: 'localhost'
},
'/api': { '/api': {
target, target,
changeOrigin: true, changeOrigin: true,
@ -54,7 +59,7 @@ module.exports = {
cookieDomainRewrite: 'localhost', cookieDomainRewrite: 'localhost',
ws: true, ws: true,
headers: { headers: {
'Origin': target Origin: target
} }
}, },
'/oauth/revoke': { '/oauth/revoke': {
@ -71,7 +76,7 @@ module.exports = {
target, target,
changeOrigin: true, changeOrigin: true,
cookieDomainRewrite: 'localhost' cookieDomainRewrite: 'localhost'
}, }
}, },
// CSS Sourcemaps off by default because relative paths are "buggy" // CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README // with this option, according to the CSS-Loader README

View file

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

View file

@ -1,6 +1,6 @@
{ {
"name": "pleroma_fe", "name": "pleroma_fe",
"version": "3.2.0", "version": "3.5.0",
"description": "A frontend for Akkoma instances", "description": "A frontend for Akkoma instances",
"author": "Roger Braun <roger@rogerbraun.net>", "author": "Roger Braun <roger@rogerbraun.net>",
"private": true, "private": true,
@ -18,19 +18,21 @@
"dependencies": { "dependencies": {
"@babel/runtime": "7.17.8", "@babel/runtime": "7.17.8",
"@chenfengyuan/vue-qrcode": "2.0.0", "@chenfengyuan/vue-qrcode": "2.0.0",
"@floatingghost/pinch-zoom-element": "^1.3.1",
"@fortawesome/fontawesome-svg-core": "1.3.0", "@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-regular-svg-icons": "^6.1.2", "@fortawesome/free-regular-svg-icons": "^6.1.2",
"@fortawesome/free-solid-svg-icons": "^6.2.0", "@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/vue-fontawesome": "3.0.1", "@fortawesome/vue-fontawesome": "3.0.1",
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
"@vuelidate/core": "^2.0.0", "@vuelidate/core": "^2.0.0",
"@vuelidate/validators": "^2.0.0", "@vuelidate/validators": "^2.0.0",
"blurhash": "^2.0.4",
"body-scroll-lock": "2.7.1", "body-scroll-lock": "2.7.1",
"chromatism": "3.0.0", "chromatism": "3.0.0",
"click-outside-vue3": "4.0.1", "click-outside-vue3": "4.0.1",
"cropperjs": "1.5.12", "cropperjs": "1.5.12",
"diff": "3.5.0", "diff": "3.5.0",
"escape-html": "1.0.3", "escape-html": "1.0.3",
"iso-639-1": "^2.1.15",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"localforage": "1.10.0", "localforage": "1.10.0",
"parse-link-header": "^2.0.0", "parse-link-header": "^2.0.0",
@ -56,7 +58,7 @@
"@vue/babel-plugin-jsx": "1.1.1", "@vue/babel-plugin-jsx": "1.1.1",
"@vue/compiler-sfc": "^3.1.0", "@vue/compiler-sfc": "^3.1.0",
"@vue/test-utils": "^2.0.2", "@vue/test-utils": "^2.0.2",
"autoprefixer": "6.7.7", "autoprefixer": "^10.4.13",
"babel-loader": "^9.1.0", "babel-loader": "^9.1.0",
"babel-plugin-lodash": "3.3.4", "babel-plugin-lodash": "3.3.4",
"chai": "^4.3.7", "chai": "^4.3.7",
@ -67,11 +69,13 @@
"css-loader": "^6.7.2", "css-loader": "^6.7.2",
"custom-event-polyfill": "^1.0.7", "custom-event-polyfill": "^1.0.7",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-standard": "^17.0.0", "eslint-config-standard": "^17.0.0",
"eslint-friendly-formatter": "^4.0.1", "eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^4.0.2", "eslint-loader": "^4.0.2",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-promise": "^6.1.1",
"eslint-plugin-standard": "^5.0.0", "eslint-plugin-standard": "^5.0.0",
"eslint-plugin-vue": "^9.7.0", "eslint-plugin-vue": "^9.7.0",
@ -82,7 +86,6 @@
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"http-proxy-middleware": "0.21.0", "http-proxy-middleware": "0.21.0",
"inject-loader": "2.0.1", "inject-loader": "2.0.1",
"iso-639-1": "2.1.15",
"isparta-loader": "2.0.0", "isparta-loader": "2.0.0",
"json-loader": "0.5.7", "json-loader": "0.5.7",
"karma": "6.3.17", "karma": "6.3.17",
@ -101,9 +104,11 @@
"nightwatch": "0.9.21", "nightwatch": "0.9.21",
"opn": "4.0.2", "opn": "4.0.2",
"ora": "0.4.1", "ora": "0.4.1",
"postcss": "^8.4.19",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"postcss-loader": "3.0.0", "postcss-loader": "^7.0.2",
"postcss-sass": "^0.5.0", "postcss-sass": "^0.5.0",
"prettier": "2.8.1",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"sass": "^1.56.0", "sass": "^1.56.0",
"sass-loader": "^13.2.0", "sass-loader": "^13.2.0",

View file

@ -1,5 +1,3 @@
module.exports = { module.exports = {
plugins: [ plugins: [require('autoprefixer')]
require('autoprefixer')
]
} }

View file

@ -1,6 +1,4 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": ["config:base"]
"config:base"
]
} }

View file

@ -24,7 +24,9 @@ export default {
components: { components: {
UserPanel, UserPanel,
NavPanel, NavPanel,
Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')), Notifications: defineAsyncComponent(() =>
import('./components/notifications/notifications.vue')
),
InstanceSpecificPanel, InstanceSpecificPanel,
FeaturesPanel, FeaturesPanel,
WhoToFollowPanel, WhoToFollowPanel,
@ -44,17 +46,20 @@ export default {
data: () => ({ data: () => ({
mobileActivePanel: 'timeline' mobileActivePanel: 'timeline'
}), }),
created () { created() {
// Load the locale from the storage // Load the locale from the storage
const val = this.$store.getters.mergedConfig.interfaceLanguage const val = this.$store.getters.mergedConfig.interfaceLanguage
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) this.$store.dispatch('setOption', {
name: 'interfaceLanguage',
value: val
})
window.addEventListener('resize', this.updateMobileState) window.addEventListener('resize', this.updateMobileState)
}, },
unmounted () { unmounted() {
window.removeEventListener('resize', this.updateMobileState) window.removeEventListener('resize', this.updateMobileState)
}, },
computed: { computed: {
classes () { classes() {
return [ return [
{ {
'-reverse': this.reverseLayout, '-reverse': this.reverseLayout,
@ -64,48 +69,76 @@ export default {
'-' + this.layoutType '-' + this.layoutType
] ]
}, },
currentUser () { return this.$store.state.users.currentUser }, currentUser() {
userBackground () { return this.currentUser.background_image }, return this.$store.state.users.currentUser
instanceBackground () { },
userBackground() {
return this.currentUser.background_image
},
instanceBackground() {
return this.mergedConfig.hideInstanceWallpaper return this.mergedConfig.hideInstanceWallpaper
? null ? null
: this.$store.state.instance.background : this.$store.state.instance.background
}, },
background () { return this.userBackground || this.instanceBackground }, background() {
bgStyle () { return this.userBackground || this.instanceBackground
},
bgStyle() {
if (this.background) { if (this.background) {
return { return {
'--body-background-image': `url(${this.background})` '--body-background-image': `url(${this.background})`
} }
} }
}, },
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, suggestionsEnabled() {
showInstanceSpecificPanel () { return this.$store.state.instance.suggestionsEnabled
return this.$store.state.instance.showInstanceSpecificPanel && },
showInstanceSpecificPanel() {
return (
this.$store.state.instance.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP && !this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent this.$store.state.instance.instanceSpecificPanelContent
)
}, },
newPostButtonShown () { newPostButtonShown() {
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile' return (
this.$store.getters.mergedConfig.alwaysShowNewPostButton ||
this.layoutType === 'mobile'
)
}, },
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, showFeaturesPanel() {
editingAvailable () { return this.$store.state.instance.editingAvailable }, return this.$store.state.instance.showFeaturesPanel
layoutType () { return this.$store.state.interface.layoutType }, },
privateMode () { return this.$store.state.instance.private }, editingAvailable() {
reverseLayout () { return this.$store.state.instance.editingAvailable
const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig },
layoutType() {
return this.$store.state.interface.layoutType
},
privateMode() {
return this.$store.state.instance.private
},
reverseLayout() {
const { thirdColumnMode, sidebarRight: reverseSetting } =
this.$store.getters.mergedConfig
if (this.layoutType !== 'wide') { if (this.layoutType !== 'wide') {
return reverseSetting return reverseSetting
} else { } else {
return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting return thirdColumnMode === 'notifications'
? reverseSetting
: !reverseSetting
} }
}, },
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders }, noSticky() {
showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars }, return this.$store.getters.mergedConfig.disableStickyHeaders
},
showScrollbars() {
return this.$store.getters.mergedConfig.showScrollbars
},
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
methods: { methods: {
updateMobileState () { updateMobileState() {
this.$store.dispatch('setLayoutWidth', windowWidth()) this.$store.dispatch('setLayoutWidth', windowWidth())
this.$store.dispatch('setLayoutHeight', windowHeight()) this.$store.dispatch('setLayoutHeight', windowHeight())
} }

View file

@ -1,6 +1,7 @@
// stylelint-disable rscss/class-format // stylelint-disable rscss/class-format
@import './_variables.scss'; @import './_variables.scss';
@import '@fortawesome/fontawesome-svg-core/styles.css';
@import '@floatingghost/pinch-zoom-element/dist/pinch-zoom.css';
:root { :root {
--navbar-height: 3.5rem; --navbar-height: 3.5rem;
--post-line-height: 1.4; --post-line-height: 1.4;
@ -12,8 +13,8 @@ html {
} }
body { body {
font-family: sans-serif; font-family: $system-sans-serif;
font-family: var(--interfaceFont, sans-serif); font-family: var(--interfaceFont, $system-sans-serif);
margin: 0; margin: 0;
color: $fallback--text; color: $fallback--text;
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
@ -22,84 +23,13 @@ body {
overscroll-behavior-y: none; overscroll-behavior-y: none;
overflow-x: clip; overflow-x: clip;
overflow-y: scroll; overflow-y: scroll;
background: var(--bg);
&.hidden { &.hidden {
display: none; display: none;
} }
} }
// ## Custom scrollbars
// Only show custom scrollbars on devices which
// have a cursor/pointer to operate them
@media (any-pointer: fine) {
* {
scrollbar-color: var(--btn) transparent;
&::-webkit-scrollbar {
background: transparent;
}
&::-webkit-scrollbar-button,
&::-webkit-scrollbar-thumb {
background-color: var(--btn);
box-shadow: var(--buttonShadow);
border-radius: var(--btnRadius);
}
// horizontal/vertical/increment/decrement are webkit-specific stuff
// that indicates whether we're affecting vertical scrollbar, increase button etc
// stylelint-disable selector-pseudo-class-no-unknown
&::-webkit-scrollbar-button {
--___bgPadding: 2px;
color: var(--btnText);
background-repeat: no-repeat, no-repeat;
&:horizontal {
background-size: 50% calc(50% - var(--___bgPadding)), 50% calc(50% - var(--___bgPadding));
&:increment {
background-image:
linear-gradient(45deg, var(--btnText) 50%, transparent 51%),
linear-gradient(-45deg, transparent 50%, var(--btnText) 51%);
background-position: top var(--___bgPadding) left 50%, right 50% bottom var(--___bgPadding);
}
&:decrement {
background-image:
linear-gradient(45deg, transparent 50%, var(--btnText) 51%),
linear-gradient(-45deg, var(--btnText) 50%, transparent 51%);
background-position: bottom var(--___bgPadding) right 50%, left 50% top var(--___bgPadding);
}
}
&:vertical {
background-size: calc(50% - var(--___bgPadding)) 50%, calc(50% - var(--___bgPadding)) 50%;
&:increment {
background-image:
linear-gradient(-45deg, transparent 50%, var(--btnText) 51%),
linear-gradient(45deg, transparent 50%, var(--btnText) 51%);
background-position: right var(--___bgPadding) top 50%, left var(--___bgPadding) top 50%;
}
&:decrement {
background-image:
linear-gradient(-45deg, var(--btnText) 50%, transparent 51%),
linear-gradient(45deg, var(--btnText) 50%, transparent 51%);
background-position: left var(--___bgPadding) top 50%, right var(--___bgPadding) top 50%;
}
}
}
// stylelint-enable selector-pseudo-class-no-unknown
}
// Body should have background to scrollbar otherwise it will use white (body color?)
html {
scrollbar-color: var(--selectedMenu) var(--wallpaper);
background: var(--wallpaper);
}
}
a { a {
text-decoration: none; text-decoration: none;
color: $fallback--link; color: $fallback--link;
@ -110,7 +40,7 @@ h4 {
margin: 0; margin: 0;
} }
i[class*=icon-], i[class*='icon-'],
.svg-inline--fa { .svg-inline--fa {
color: $fallback--icon; color: $fallback--icon;
color: var(--icon, $fallback--icon); color: var(--icon, $fallback--icon);
@ -128,6 +58,7 @@ nav {
box-sizing: border-box; box-sizing: border-box;
height: var(--navbar-height); height: var(--navbar-height);
position: fixed; position: fixed;
backdrop-filter: blur(12px) saturate(1.2);
} }
#sidebar { #sidebar {
@ -182,7 +113,7 @@ nav {
position: relative; position: relative;
display: grid; display: grid;
grid-template-columns: var(--miniColumn) var(--maxiColumn); grid-template-columns: var(--miniColumn) var(--maxiColumn);
grid-template-areas: "sidebar content"; grid-template-areas: 'sidebar content';
grid-template-rows: 1fr; grid-template-rows: 1fr;
box-sizing: border-box; box-sizing: border-box;
margin: 0 auto; margin: 0 auto;
@ -191,6 +122,7 @@ nav {
justify-content: center; justify-content: center;
min-height: 100vh; min-height: 100vh;
overflow-x: clip; overflow-x: clip;
padding: 0 calc(var(--columnGap) / 2);
.column { .column {
--___columnMargin: var(--columnGap); --___columnMargin: var(--columnGap);
@ -228,7 +160,9 @@ nav {
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
margin-left: calc(var(--___paddingIncrease) * -1); margin-left: calc(var(--___paddingIncrease) * -1);
padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2); padding-left: calc(
var(--___paddingIncrease) + var(--___columnMargin) / 2
);
// On browsers that don't support hiding scrollbars we enforce "show scrolbars" mode // On browsers that don't support hiding scrollbars we enforce "show scrolbars" mode
// might implement old style of hiding scrollbars later if there's demand // might implement old style of hiding scrollbars later if there's demand
@ -236,7 +170,9 @@ nav {
&:not(.-show-scrollbar) { &:not(.-show-scrollbar) {
scrollbar-width: none; scrollbar-width: none;
margin-right: calc(var(--___paddingIncrease) * -1); margin-right: calc(var(--___paddingIncrease) * -1);
padding-right: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2); padding-right: calc(
var(--___paddingIncrease) + var(--___columnMargin) / 2
);
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: block; display: block;
@ -276,21 +212,21 @@ nav {
&.-reverse:not(.-wide):not(.-mobile) { &.-reverse:not(.-wide):not(.-mobile) {
grid-template-columns: var(--maxiColumn) var(--miniColumn); grid-template-columns: var(--maxiColumn) var(--miniColumn);
grid-template-areas: "content sidebar"; grid-template-areas: 'content sidebar';
} }
&.-wide { &.-wide {
grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn); grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn);
grid-template-areas: "sidebar content notifs"; grid-template-areas: 'sidebar content notifs';
&.-reverse { &.-reverse {
grid-template-areas: "notifs content sidebar"; grid-template-areas: 'notifs content sidebar';
} }
} }
&.-mobile { &.-mobile {
grid-template-columns: 100vw; grid-template-columns: 100vw;
grid-template-areas: "content"; grid-template-areas: 'content';
padding: 0; padding: 0;
.column { .column {
@ -347,7 +283,7 @@ nav {
background: transparent; background: transparent;
} }
i[class*=icon-], i[class*='icon-'],
.svg-inline--fa { .svg-inline--fa {
color: $fallback--text; color: $fallback--text;
color: var(--btnText, $fallback--text); color: var(--btnText, $fallback--text);
@ -363,7 +299,9 @@ nav {
} }
&:active { &:active {
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset; box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3),
0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow); box-shadow: var(--buttonPressedShadow);
color: $fallback--text; color: $fallback--text;
color: var(--btnPressedText, $fallback--text); color: var(--btnPressedText, $fallback--text);
@ -396,7 +334,9 @@ nav {
color: var(--btnToggledText, $fallback--text); color: var(--btnToggledText, $fallback--text);
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--btnToggled, $fallback--fg); background-color: var(--btnToggled, $fallback--fg);
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset; box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3),
0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow); box-shadow: var(--buttonPressedShadow);
svg, svg,
@ -461,14 +401,15 @@ textarea,
border: none; border: none;
border-radius: $fallback--inputRadius; border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius); border-radius: var(--inputRadius, $fallback--inputRadius);
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset; box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset;
box-shadow: var(--inputShadow); box-shadow: var(--inputShadow);
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--input, $fallback--fg); background-color: var(--input, $fallback--fg);
color: $fallback--lightText; color: $fallback--lightText;
color: var(--inputText, $fallback--lightText); color: var(--inputText, $fallback--lightText);
font-family: sans-serif; font-family: sans-serif;
font-family: var(--inputFont, sans-serif); font-family: var(--interfaceFont, sans-serif);
font-size: 1em; font-size: 1em;
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;
@ -479,13 +420,13 @@ textarea,
padding: 0 var(--_padding); padding: 0 var(--_padding);
&:disabled, &:disabled,
&[disabled=disabled], &[disabled='disabled'],
&.disabled { &.disabled {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.5; opacity: 0.5;
} }
&[type=range] { &[type='range'] {
background: none; background: none;
border: none; border: none;
margin: 0; margin: 0;
@ -493,12 +434,13 @@ textarea,
flex: 1; flex: 1;
} }
&[type=radio] { &[type='radio'] {
display: none; display: none;
&:checked + label::before { &:checked + label::before {
box-shadow: 0 0 2px black inset, 0 0 0 4px $fallback--fg inset; box-shadow: 0 0 2px black inset, 0 0 0 4px $fallback--fg inset;
box-shadow: var(--inputShadow), 0 0 0 4px var(--fg, $fallback--fg) inset; box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset,
0 0 0 4px var(--fg, $fallback--fg) inset;
background-color: var(--accent, $fallback--link); background-color: var(--accent, $fallback--link);
} }
@ -519,7 +461,7 @@ textarea,
height: 1.1em; height: 1.1em;
border-radius: 100%; // Radio buttons should always be circle border-radius: 100%; // Radio buttons should always be circle
box-shadow: 0 0 2px black inset; box-shadow: 0 0 2px black inset;
box-shadow: var(--inputShadow); box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset;
margin-right: 0.5em; margin-right: 0.5em;
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--input, $fallback--fg); background-color: var(--input, $fallback--fg);
@ -533,7 +475,7 @@ textarea,
} }
} }
&[type=checkbox] { &[type='checkbox'] {
display: none; display: none;
&:checked + label::before { &:checked + label::before {
@ -559,7 +501,7 @@ textarea,
border-radius: $fallback--checkboxRadius; border-radius: $fallback--checkboxRadius;
border-radius: var(--checkboxRadius, $fallback--checkboxRadius); border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
box-shadow: 0 0 2px black inset; box-shadow: 0 0 2px black inset;
box-shadow: var(--inputShadow); box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset;
margin-right: 0.5em; margin-right: 0.5em;
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--input, $fallback--fg); background-color: var(--input, $fallback--fg);
@ -594,8 +536,8 @@ option {
.hide-number-spinner { .hide-number-spinner {
-moz-appearance: textfield; -moz-appearance: textfield;
&[type=number]::-webkit-inner-spin-button, &[type='number']::-webkit-inner-spin-button,
&[type=number]::-webkit-outer-spin-button { &[type='number']::-webkit-outer-spin-button {
opacity: 0; opacity: 0;
display: none; display: none;
} }

View file

@ -43,7 +43,7 @@
:to="{ name: 'login' }" :to="{ name: 'login' }"
class="panel-body" class="panel-body"
> >
{{ $t("login.hint") }} {{ $t('login.hint') }}
</router-link> </router-link>
</div> </div>
<router-view /> <router-view />

View file

@ -4,7 +4,7 @@ $darkened-background: whitesmoke;
$fallback--bg: #121a24; $fallback--bg: #121a24;
$fallback--fg: #182230; $fallback--fg: #182230;
$fallback--faint: rgba(185, 185, 186, .5); $fallback--faint: rgba(185, 185, 186, 0.5);
$fallback--text: #b9b9ba; $fallback--text: #b9b9ba;
$fallback--link: #d8a070; $fallback--link: #d8a070;
$fallback--icon: #666; $fallback--icon: #666;
@ -16,8 +16,8 @@ $fallback--cBlue: #0095ff;
$fallback--cGreen: #0fa00f; $fallback--cGreen: #0fa00f;
$fallback--cOrange: orange; $fallback--cOrange: orange;
$fallback--alertError: rgba(211,16,20,.5); $fallback--alertError: rgba(211, 16, 20, 0.5);
$fallback--alertWarning: rgba(111,111,20,.5); $fallback--alertWarning: rgba(111, 111, 20, 0.5);
$fallback--panelRadius: 10px; $fallback--panelRadius: 10px;
$fallback--checkboxRadius: 2px; $fallback--checkboxRadius: 2px;
@ -28,6 +28,14 @@ $fallback--avatarRadius: 4px;
$fallback--avatarAltRadius: 10px; $fallback--avatarAltRadius: 10px;
$fallback--attachmentRadius: 10px; $fallback--attachmentRadius: 10px;
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; $fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1),
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
$status-margin: 0.75em; $status-margin: 0.75em;
$system-sans-serif: -apple-system, BlinkMacSystemFont, avenir next, avenir,
segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial,
sans-serif;
$system-mono: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console,
monospace;

View file

@ -3,13 +3,21 @@ import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import vClickOutside from 'click-outside-vue3' import vClickOutside from 'click-outside-vue3'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome' import {
FontAwesomeIcon,
FontAwesomeLayers
} from '@fortawesome/vue-fontawesome'
import { config } from '@fortawesome/fontawesome-svg-core'
config.autoAddCss = false
import App from '../App.vue' import App from '../App.vue'
import routes from './routes' import routes from './routes'
import VBodyScrollLock from 'src/directives/body_scroll_lock' import VBodyScrollLock from 'src/directives/body_scroll_lock'
import { windowWidth, windowHeight } from '../services/window_utils/window_utils' import {
windowWidth,
windowHeight
} from '../services/window_utils/window_utils'
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js' import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
@ -23,7 +31,9 @@ const parsedInitialResults = () => {
return null return null
} }
if (!staticInitialResults) { if (!staticInitialResults) {
staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent) staticInitialResults = JSON.parse(
document.getElementById('initial-results').textContent
)
} }
return staticInitialResults return staticInitialResults
} }
@ -71,18 +81,30 @@ const getInstanceConfig = async ({ store }) => {
const textlimit = data.max_toot_chars const textlimit = data.max_toot_chars
const vapidPublicKey = data.pleroma.vapid_public_key const vapidPublicKey = data.pleroma.vapid_public_key
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required }) name: 'textlimit',
value: textlimit
})
store.dispatch('setInstanceOption', {
name: 'accountApprovalRequired',
value: data.approval_required
})
// don't override cookie if set // don't override cookie if set
if (!Cookies.get('userLanguage')) { if (!Cookies.get('userLanguage')) {
store.dispatch('setOption', { name: 'interfaceLanguage', value: resolveLanguage(data.languages) }) store.dispatch('setOption', {
name: 'interfaceLanguage',
value: resolveLanguage(data.languages)
})
} }
if (vapidPublicKey) { if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) store.dispatch('setInstanceOption', {
name: 'vapidPublicKey',
value: vapidPublicKey
})
} }
} else { } else {
throw (res) throw res
} }
} catch (error) { } catch (error) {
console.error('Could not load instance config, potentially fatal') console.error('Could not load instance config, potentially fatal')
@ -97,10 +119,12 @@ const getBackendProvidedConfig = async ({ store }) => {
const data = await res.json() const data = await res.json()
return data.pleroma_fe return data.pleroma_fe
} else { } else {
throw (res) throw res
} }
} catch (error) { } catch (error) {
console.error('Could not load backend-provided frontend config, potentially fatal') console.error(
'Could not load backend-provided frontend config, potentially fatal'
)
console.error(error) console.error(error)
} }
} }
@ -111,7 +135,7 @@ const getStaticConfig = async () => {
if (res.ok) { if (res.ok) {
return res.json() return res.json()
} else { } else {
throw (res) throw res
} }
} catch (error) { } catch (error) {
console.warn('Failed to load static/config.json, continuing without it.') console.warn('Failed to load static/config.json, continuing without it.')
@ -154,16 +178,12 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', {
name: 'logoMask', name: 'logoMask',
value: typeof config.logoMask === 'undefined' value: typeof config.logoMask === 'undefined' ? true : config.logoMask
? true
: config.logoMask
}) })
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', {
name: 'logoMargin', name: 'logoMargin',
value: typeof config.logoMargin === 'undefined' value: typeof config.logoMargin === 'undefined' ? 0 : config.logoMargin
? 0
: config.logoMargin
}) })
copyInstanceOption('logoLeft') copyInstanceOption('logoLeft')
store.commit('authFlow/setInitialStrategy', config.loginMethod) store.commit('authFlow/setInitialStrategy', config.loginMethod)
@ -191,7 +211,7 @@ const getTOS = async ({ store }) => {
const html = await res.text() const html = await res.text()
store.dispatch('setInstanceOption', { name: 'tos', value: html }) store.dispatch('setInstanceOption', { name: 'tos', value: html })
} else { } else {
throw (res) throw res
} }
} catch (e) { } catch (e) {
console.warn("Can't load TOS") console.warn("Can't load TOS")
@ -204,9 +224,12 @@ const getInstancePanel = async ({ store }) => {
const res = await preloadFetch('/instance/panel.html') const res = await preloadFetch('/instance/panel.html')
if (res.ok) { if (res.ok) {
const html = await res.text() const html = await res.text()
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html }) store.dispatch('setInstanceOption', {
name: 'instanceSpecificPanelContent',
value: html
})
} else { } else {
throw (res) throw res
} }
} catch (e) { } catch (e) {
console.warn("Can't load instance panel") console.warn("Can't load instance panel")
@ -219,25 +242,30 @@ const getStickers = async ({ store }) => {
const res = await window.fetch('/static/stickers.json') const res = await window.fetch('/static/stickers.json')
if (res.ok) { if (res.ok) {
const values = await res.json() const values = await res.json()
const stickers = (await Promise.all( const stickers = (
Object.entries(values).map(async ([name, path]) => { await Promise.all(
const resPack = await window.fetch(path + 'pack.json') Object.entries(values).map(async ([name, path]) => {
var meta = {} const resPack = await window.fetch(path + 'pack.json')
if (resPack.ok) { var meta = {}
meta = await resPack.json() if (resPack.ok) {
} meta = await resPack.json()
return { }
pack: name, return {
path, pack: name,
meta path,
} meta
}) }
)).sort((a, b) => { })
)
).sort((a, b) => {
return a.meta.title.localeCompare(b.meta.title) return a.meta.title.localeCompare(b.meta.title)
}) })
store.dispatch('setInstanceOption', { name: 'stickers', value: stickers }) store.dispatch('setInstanceOption', {
name: 'stickers',
value: stickers
})
} else { } else {
throw (res) throw res
} }
} catch (e) { } catch (e) {
console.warn("Can't load stickers") console.warn("Can't load stickers")
@ -252,13 +280,19 @@ const getAppSecret = async ({ store }) => {
.then((app) => getClientToken({ ...app, instance: instance.server })) .then((app) => getClientToken({ ...app, instance: instance.server }))
.then((token) => { .then((token) => {
commit('setAppToken', token.access_token) commit('setAppToken', token.access_token)
commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) commit(
'setBackendInteractor',
backendInteractorService(store.getters.getToken())
)
}) })
} }
const resolveStaffAccounts = ({ store, accounts }) => { const resolveStaffAccounts = ({ store, accounts }) => {
const nicknames = accounts.map(uri => uri.split('/').pop()) const nicknames = accounts.map((uri) => uri.split('/').pop())
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames }) store.dispatch('setInstanceOption', {
name: 'staffAccounts',
value: nicknames
})
} }
const getNodeInfo = async ({ store }) => { const getNodeInfo = async ({ store }) => {
@ -268,65 +302,146 @@ const getNodeInfo = async ({ store }) => {
const data = await res.json() const data = await res.json()
const metadata = data.metadata const metadata = data.metadata
const features = metadata.features const features = metadata.features
store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations }) name: 'name',
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) value: metadata.nodeName
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') }) })
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') }) name: 'registrationOpen',
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) value: data.openRegistrations
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) })
store.dispatch('setInstanceOption', { name: 'translationEnabled', value: features.includes('akkoma:machine_translation') }) store.dispatch('setInstanceOption', {
name: 'mediaProxyAvailable',
value: features.includes('media_proxy')
})
store.dispatch('setInstanceOption', {
name: 'safeDM',
value: features.includes('safe_dm_mentions')
})
store.dispatch('setInstanceOption', {
name: 'pollsAvailable',
value: features.includes('polls')
})
store.dispatch('setInstanceOption', {
name: 'editingAvailable',
value: features.includes('editing')
})
store.dispatch('setInstanceOption', {
name: 'pollLimits',
value: metadata.pollLimits
})
store.dispatch('setInstanceOption', {
name: 'mailerEnabled',
value: metadata.mailerEnabled
})
store.dispatch('setInstanceOption', {
name: 'translationEnabled',
value: features.includes('akkoma:machine_translation')
})
const uploadLimits = metadata.uploadLimits const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) }) name: 'uploadlimit',
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) }) value: parseInt(uploadLimits.general)
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) }) })
store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits }) store.dispatch('setInstanceOption', {
name: 'avatarlimit',
value: parseInt(uploadLimits.avatar)
})
store.dispatch('setInstanceOption', {
name: 'backgroundlimit',
value: parseInt(uploadLimits.background)
})
store.dispatch('setInstanceOption', {
name: 'bannerlimit',
value: parseInt(uploadLimits.banner)
})
store.dispatch('setInstanceOption', {
name: 'fieldsLimits',
value: metadata.fieldsLimits
})
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats }) name: 'restrictedNicknames',
value: metadata.restrictedNicknames
})
store.dispatch('setInstanceOption', {
name: 'postFormats',
value: metadata.postFormats
})
const suggestions = metadata.suggestions const suggestions = metadata.suggestions
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web }) name: 'suggestionsEnabled',
value: suggestions.enabled
})
store.dispatch('setInstanceOption', {
name: 'suggestionsWeb',
value: suggestions.web
})
const software = data.software const software = data.software
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' }) name: 'backendVersion',
value: software.version
})
store.dispatch('setInstanceOption', {
name: 'pleromaBackend',
value: software.name === 'pleroma'
})
const priv = metadata.private const priv = metadata.private
store.dispatch('setInstanceOption', { name: 'private', value: priv }) store.dispatch('setInstanceOption', { name: 'private', value: priv })
const frontendVersion = window.___pleromafe_commit_hash const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion }) store.dispatch('setInstanceOption', {
name: 'frontendVersion',
value: frontendVersion
})
const federation = metadata.federation const federation = metadata.federation
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', {
name: 'tagPolicyAvailable', name: 'tagPolicyAvailable',
value: typeof federation.mrf_policies === 'undefined' value:
? false typeof federation.mrf_policies === 'undefined'
: metadata.federation.mrf_policies.includes('TagPolicy') ? false
: metadata.federation.mrf_policies.includes('TagPolicy')
}) })
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation }) store.dispatch('setInstanceOption', {
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances }) name: 'federationPolicy',
value: federation
})
store.dispatch('setInstanceOption', {
name: 'localBubbleInstances',
value: metadata.localBubbleInstances
})
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', {
name: 'federating', name: 'federating',
value: typeof federation.enabled === 'undefined' value:
? true typeof federation.enabled === 'undefined' ? true : federation.enabled
: federation.enabled })
store.dispatch('setInstanceOption', {
name: 'publicTimelineVisibility',
value: metadata.publicTimelineVisibility
})
store.dispatch('setInstanceOption', {
name: 'federatedTimelineAvailable',
value: metadata.federatedTimelineAvailable
}) })
const accountActivationRequired = metadata.accountActivationRequired const accountActivationRequired = metadata.accountActivationRequired
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired }) store.dispatch('setInstanceOption', {
name: 'accountActivationRequired',
value: accountActivationRequired
})
const accounts = metadata.staffAccounts const accounts = metadata.staffAccounts
resolveStaffAccounts({ store, accounts }) resolveStaffAccounts({ store, accounts })
} else { } else {
throw (res) throw res
} }
} catch (e) { } catch (e) {
console.warn('Could not load nodeinfo') console.warn('Could not load nodeinfo')
@ -336,11 +451,16 @@ const getNodeInfo = async ({ store }) => {
const setConfig = async ({ store }) => { const setConfig = async ({ store }) => {
// apiConfig, staticConfig // apiConfig, staticConfig
const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()]) const configInfos = await Promise.all([
getBackendProvidedConfig({ store }),
getStaticConfig()
])
const apiConfig = configInfos[0] const apiConfig = configInfos[0]
const staticConfig = configInfos[1] const staticConfig = configInfos[1]
await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store })) await setSettings({ store, apiConfig, staticConfig }).then(
getAppSecret({ store })
)
} }
const checkOAuthToken = async ({ store }) => { const checkOAuthToken = async ({ store }) => {
@ -363,7 +483,10 @@ const afterStoreSetup = async ({ store, i18n }) => {
FaviconService.initFaviconService() FaviconService.initFaviconService()
const overrides = window.___pleromafe_dev_overrides || {} const overrides = window.___pleromafe_dev_overrides || {}
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin const server =
typeof overrides.target !== 'undefined'
? overrides.target
: window.location.origin
store.dispatch('setInstanceOption', { name: 'server', value: server }) store.dispatch('setInstanceOption', { name: 'server', value: server })
await setConfig({ store }) await setConfig({ store })
@ -373,7 +496,10 @@ const afterStoreSetup = async ({ store, i18n }) => {
const customThemePresent = customThemeSource || customTheme const customThemePresent = customThemeSource || customTheme
if (customThemePresent) { if (customThemePresent) {
if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) { if (
customThemeSource &&
customThemeSource.themeEngineVersion === CURRENT_VERSION
) {
applyTheme(customThemeSource) applyTheme(customThemeSource)
} else { } else {
applyTheme(customTheme) applyTheme(customTheme)
@ -394,9 +520,6 @@ const afterStoreSetup = async ({ store, i18n }) => {
]) ])
// Start fetching things that don't need to block the UI // Start fetching things that don't need to block the UI
store.dispatch('fetchMutes')
store.dispatch('startFetchingAnnouncements')
store.dispatch('startFetchingReports')
getTOS({ store }) getTOS({ store })
getStickers({ store }) getStickers({ store })
@ -404,7 +527,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
history: createWebHistory(), history: createWebHistory(),
routes: routes(store), routes: routes(store),
scrollBehavior: (to, _from, savedPosition) => { scrollBehavior: (to, _from, savedPosition) => {
if (to.matched.some(m => m.meta.dontScroll)) { if (to.matched.some((m) => m.meta.dontScroll)) {
return {} return {}
} }

View file

@ -35,51 +35,145 @@ export default (store) => {
} }
let routes = [ let routes = [
{ name: 'root', {
name: 'root',
path: '/', path: '/',
redirect: _to => { redirect: (_to) => {
return (store.state.users.currentUser return (
? store.state.instance.redirectRootLogin (store.state.users.currentUser
: store.state.instance.redirectRootNoLogin) || '/main/all' ? store.state.instance.redirectRootLogin
: store.state.instance.redirectRootNoLogin) || '/main/all'
)
} }
}, },
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline }, {
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline }, name: 'public-external-timeline',
{ name: 'bubble-timeline', path: '/main/bubble', component: BubbleTimeline }, path: '/main/all',
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute }, component: PublicAndExternalTimeline
},
{
name: 'public-timeline',
path: '/main/public',
component: PublicTimeline
},
{
name: 'bubble-timeline',
path: '/main/bubble',
component: BubbleTimeline
},
{
name: 'friends',
path: '/main/friends',
component: FriendsTimeline,
beforeEnter: validateAuthenticatedRoute
},
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline }, { name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline }, { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, {
{ name: 'remote-user-profile-acct', name: 'conversation',
path: '/notice/:id',
component: ConversationPage,
meta: { dontScroll: true }
},
{
name: 'remote-user-profile-acct',
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)', path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
component: RemoteUserResolver, component: RemoteUserResolver,
beforeEnter: validateAuthenticatedRoute beforeEnter: validateAuthenticatedRoute
}, },
{ name: 'remote-user-profile', {
name: 'remote-user-profile',
path: '/remote-users/:hostname/:username', path: '/remote-users/:hostname/:username',
component: RemoteUserResolver, component: RemoteUserResolver,
beforeEnter: validateAuthenticatedRoute beforeEnter: validateAuthenticatedRoute
}, },
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile, meta: { dontScroll: true } }, {
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute }, name: 'external-user-profile',
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, path: '/users/:id',
component: UserProfile,
meta: { dontScroll: true }
},
{
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', 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: 'registration-request-sent',
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true }, path: '/registration-request-sent',
{ name: 'registration-token', path: '/registration/:token', component: Registration }, component: RegistrationRequestSent
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, },
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute }, {
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
},
{
name: 'notifications',
path: '/:username/notifications',
component: Notifications,
props: () => ({ disableTeleport: true }),
beforeEnter: validateAuthenticatedRoute
},
{ name: 'login', path: '/login', component: AuthForm }, { name: 'login', path: '/login', component: AuthForm },
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }, {
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) }, name: 'oauth-callback',
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute }, path: '/oauth-callback',
component: OAuthCallback,
props: (route) => ({ code: route.query.code })
},
{
name: 'search',
path: '/search',
component: Search,
props: (route) => ({ query: route.query.query })
},
{
name: 'who-to-follow',
path: '/who-to-follow',
component: WhoToFollow,
beforeEnter: validateAuthenticatedRoute
},
{ name: 'about', path: '/about', component: About }, { name: 'about', path: '/about', component: About },
{ name: 'lists', path: '/lists', component: Lists }, { name: 'lists', path: '/lists', component: Lists },
{ name: 'list-timeline', path: '/lists/:id', component: ListTimeline }, { name: 'list-timeline', path: '/lists/:id', component: ListTimeline },
{ name: 'list-edit', path: '/lists/:id/edit', component: ListEdit }, { name: 'list-edit', path: '/lists/:id/edit', component: ListEdit },
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage }, {
{ name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile, meta: { dontScroll: true } } name: 'announcements',
path: '/announcements',
component: AnnouncementsPage
},
{
name: 'user-profile',
path: '/:_(users)?/:name',
component: UserProfile,
meta: { dontScroll: true }
}
] ]
return routes return routes

View file

@ -15,13 +15,17 @@ const About = {
LocalBubblePanel LocalBubblePanel
}, },
computed: { computed: {
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, showFeaturesPanel() {
showInstanceSpecificPanel () { return this.$store.state.instance.showFeaturesPanel
return this.$store.state.instance.showInstanceSpecificPanel && },
showInstanceSpecificPanel() {
return (
this.$store.state.instance.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP && !this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent this.$store.state.instance.instanceSpecificPanelContent
)
}, },
showLocalBubblePanel () { showLocalBubblePanel() {
return this.$store.state.instance.localBubbleInstances.length > 0 return this.$store.state.instance.localBubbleInstances.length > 0
} }
} }

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ const Announcement = {
AnnouncementEditor, AnnouncementEditor,
RichContent RichContent
}, },
data () { data() {
return { return {
editing: false, editing: false,
editedAnnouncement: { editedAnnouncement: {
@ -25,78 +25,93 @@ const Announcement = {
}, },
computed: { computed: {
...mapState({ ...mapState({
currentUser: state => state.users.currentUser currentUser: (state) => state.users.currentUser
}), }),
content () { content() {
return this.announcement.content return this.announcement.content
}, },
isRead () { isRead() {
return this.announcement.read return this.announcement.read
}, },
publishedAt () { publishedAt() {
const time = this.announcement['published_at'] const time = this.announcement['published_at']
if (!time) { if (!time) {
return return
} }
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale)) return this.formatTimeOrDate(
time,
localeService.internalToBrowserLocale(this.$i18n.locale)
)
}, },
startsAt () { startsAt() {
const time = this.announcement['starts_at'] const time = this.announcement['starts_at']
if (!time) { if (!time) {
return return
} }
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale)) return this.formatTimeOrDate(
time,
localeService.internalToBrowserLocale(this.$i18n.locale)
)
}, },
endsAt () { endsAt() {
const time = this.announcement['ends_at'] const time = this.announcement['ends_at']
if (!time) { if (!time) {
return return
} }
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale)) return this.formatTimeOrDate(
time,
localeService.internalToBrowserLocale(this.$i18n.locale)
)
}, },
inactive () { inactive() {
return this.announcement.inactive return this.announcement.inactive
} }
}, },
methods: { methods: {
markAsRead () { markAsRead() {
if (!this.isRead) { if (!this.isRead) {
return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id) return this.$store.dispatch(
'markAnnouncementAsRead',
this.announcement.id
)
} }
}, },
deleteAnnouncement () { deleteAnnouncement() {
return this.$store.dispatch('deleteAnnouncement', this.announcement.id) return this.$store.dispatch('deleteAnnouncement', this.announcement.id)
}, },
formatTimeOrDate (time, locale) { formatTimeOrDate(time, locale) {
const d = new Date(time) const d = new Date(time)
return this.announcement['all_day'] ? d.toLocaleDateString(locale) : d.toLocaleString(locale) return this.announcement['all_day']
? d.toLocaleDateString(locale)
: d.toLocaleString(locale)
}, },
enterEditMode () { enterEditMode() {
this.editedAnnouncement.content = this.announcement.pleroma['raw_content'] this.editedAnnouncement.content = this.announcement.pleroma['raw_content']
this.editedAnnouncement.startsAt = this.announcement['starts_at'] this.editedAnnouncement.startsAt = this.announcement['starts_at']
this.editedAnnouncement.endsAt = this.announcement['ends_at'] this.editedAnnouncement.endsAt = this.announcement['ends_at']
this.editedAnnouncement.allDay = this.announcement['all_day'] this.editedAnnouncement.allDay = this.announcement['all_day']
this.editing = true this.editing = true
}, },
submitEdit () { submitEdit() {
this.$store.dispatch('editAnnouncement', { this.$store
id: this.announcement.id, .dispatch('editAnnouncement', {
...this.editedAnnouncement id: this.announcement.id,
}) ...this.editedAnnouncement
})
.then(() => { .then(() => {
this.editing = false this.editing = false
}) })
.catch(error => { .catch((error) => {
this.editError = error.error this.editError = error.error
}) })
}, },
cancelEdit () { cancelEdit() {
this.editing = false this.editing = false
}, },
clearError () { clearError() {
this.editError = undefined this.editError = undefined
} }
} }

View file

@ -21,7 +21,9 @@
class="times" class="times"
> >
<span v-if="publishedAt"> <span v-if="publishedAt">
{{ $t('announcements.published_time_display', { time: publishedAt }) }} {{
$t('announcements.published_time_display', { time: publishedAt })
}}
</span> </span>
<span v-if="startsAt"> <span v-if="startsAt">
{{ $t('announcements.start_time_display', { time: startsAt }) }} {{ $t('announcements.start_time_display', { time: startsAt }) }}
@ -99,7 +101,7 @@
<script src="./announcement.js"></script> <script src="./announcement.js"></script>
<style lang="scss"> <style lang="scss">
@import "../../variables"; @import '../../variables';
.announcement { .announcement {
border-bottom-width: 1px; border-bottom-width: 1px;
@ -108,7 +110,8 @@
border-radius: 0; border-radius: 0;
padding: var(--status-margin, $status-margin); padding: var(--status-margin, $status-margin);
.heading, .body { .heading,
.body {
margin-bottom: var(--status-margin, $status-margin); margin-bottom: var(--status-margin, $status-margin);
} }

View file

@ -10,22 +10,26 @@
:disabled="disabled" :disabled="disabled"
/> />
<span class="announcement-metadata"> <span class="announcement-metadata">
<label for="announcement-start-time">{{ $t('announcements.start_time_prompt') }}</label> <label for="announcement-start-time">{{
$t('announcements.start_time_prompt')
}}</label>
<input <input
id="announcement-start-time" id="announcement-start-time"
v-model="announcement.startsAt" v-model="announcement.startsAt"
:type="announcement.allDay ? 'date' : 'datetime-local'" :type="announcement.allDay ? 'date' : 'datetime-local'"
:disabled="disabled" :disabled="disabled"
> />
</span> </span>
<span class="announcement-metadata"> <span class="announcement-metadata">
<label for="announcement-end-time">{{ $t('announcements.end_time_prompt') }}</label> <label for="announcement-end-time">{{
$t('announcements.end_time_prompt')
}}</label>
<input <input
id="announcement-end-time" id="announcement-end-time"
v-model="announcement.endsAt" v-model="announcement.endsAt"
:type="announcement.allDay ? 'date' : 'datetime-local'" :type="announcement.allDay ? 'date' : 'datetime-local'"
:disabled="disabled" :disabled="disabled"
> />
</span> </span>
<span class="announcement-metadata"> <span class="announcement-metadata">
<Checkbox <Checkbox
@ -33,7 +37,9 @@
v-model="announcement.allDay" v-model="announcement.allDay"
:disabled="disabled" :disabled="disabled"
/> />
<label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label> <label for="announcement-all-day">{{
$t('announcements.all_day_prompt')
}}</label>
</span> </span>
</div> </div>
</template> </template>

View file

@ -7,7 +7,7 @@ const AnnouncementsPage = {
Announcement, Announcement,
AnnouncementEditor AnnouncementEditor
}, },
data () { data() {
return { return {
newAnnouncement: { newAnnouncement: {
content: '', content: '',
@ -19,34 +19,35 @@ const AnnouncementsPage = {
error: undefined error: undefined
} }
}, },
mounted () { mounted() {
this.$store.dispatch('fetchAnnouncements') this.$store.dispatch('fetchAnnouncements')
}, },
computed: { computed: {
...mapState({ ...mapState({
currentUser: state => state.users.currentUser currentUser: (state) => state.users.currentUser
}), }),
announcements () { announcements() {
return this.$store.state.announcements.announcements return this.$store.state.announcements.announcements
} }
}, },
methods: { methods: {
postAnnouncement () { postAnnouncement() {
this.posting = true this.posting = true
this.$store.dispatch('postAnnouncement', this.newAnnouncement) this.$store
.dispatch('postAnnouncement', this.newAnnouncement)
.then(() => { .then(() => {
this.newAnnouncement.content = '' this.newAnnouncement.content = ''
this.startsAt = undefined this.startsAt = undefined
this.endsAt = undefined this.endsAt = undefined
}) })
.catch(error => { .catch((error) => {
this.error = error.error this.error = error.error
}) })
.finally(() => { .finally(() => {
this.posting = false this.posting = false
}) })
}, },
clearError () { clearError() {
this.error = undefined this.error = undefined
} }
} }

View file

@ -6,9 +6,7 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<section <section v-if="currentUser && currentUser.role === 'admin'">
v-if="currentUser && currentUser.role === 'admin'"
>
<div class="post-form"> <div class="post-form">
<div class="heading"> <div class="heading">
<h4>{{ $t('announcements.post_form_header') }}</h4> <h4>{{ $t('announcements.post_form_header') }}</h4>
@ -50,9 +48,7 @@
v-for="announcement in announcements" v-for="announcement in announcements"
:key="announcement.id" :key="announcement.id"
> >
<announcement <announcement :announcement="announcement" />
:announcement="announcement"
/>
</section> </section>
</div> </div>
</div> </div>
@ -61,13 +57,14 @@
<script src="./announcements_page.js"></script> <script src="./announcements_page.js"></script>
<style lang="scss"> <style lang="scss">
@import "../../variables"; @import '../../variables';
.announcements-page { .announcements-page {
.post-form { .post-form {
padding: var(--status-margin, $status-margin); padding: var(--status-margin, $status-margin);
.heading, .body { .heading,
.body {
margin-bottom: var(--status-margin, $status-margin); margin-bottom: var(--status-margin, $status-margin);
} }

View file

@ -21,7 +21,7 @@
export default { export default {
emits: ['resetAsyncComponent'], emits: ['resetAsyncComponent'],
methods: { methods: {
retry () { retry() {
this.$emit('resetAsyncComponent') this.$emit('resetAsyncComponent')
} }
} }
@ -35,8 +35,8 @@ export default {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
.btn { .btn {
margin: .5em; margin: 0.5em;
padding: .5em 2em; padding: 0.5em 2em;
} }
} }
</style> </style>

View file

@ -18,6 +18,7 @@ import {
faPencilAlt, faPencilAlt,
faAlignRight faAlignRight
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import Blurhash from '../blurhash/Blurhash.vue'
library.add( library.add(
faFile, faFile,
@ -46,14 +47,16 @@ const Attachment = {
'shiftDn', 'shiftDn',
'edit' 'edit'
], ],
data () { data() {
return { return {
localDescription: this.description || this.attachment.description, localDescription: this.description || this.attachment.description,
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage, nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw, hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
preloadImage: this.$store.getters.mergedConfig.preloadImage, preloadImage: this.$store.getters.mergedConfig.preloadImage,
loading: false, loading: false,
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'), img:
fileTypeService.fileType(this.attachment.mimetype) === 'image' &&
document.createElement('img'),
modalOpen: false, modalOpen: false,
showHidden: false, showHidden: false,
flashLoaded: false, flashLoaded: false,
@ -63,10 +66,11 @@ const Attachment = {
components: { components: {
Flash, Flash,
StillImage, StillImage,
VideoAttachment VideoAttachment,
Blurhash
}, },
computed: { computed: {
classNames () { classNames() {
return [ return [
{ {
'-loading': this.loading, '-loading': this.loading,
@ -78,37 +82,40 @@ const Attachment = {
`-${this.useContainFit ? 'contain' : 'cover'}-fit` `-${this.useContainFit ? 'contain' : 'cover'}-fit`
] ]
}, },
usePlaceholder () { usePlaceholder() {
return this.size === 'hide' return this.size === 'hide'
}, },
useContainFit () { useContainFit() {
return this.$store.getters.mergedConfig.useContainFit return this.$store.getters.mergedConfig.useContainFit
}, },
placeholderName () { useBlurhash() {
return this.$store.getters.mergedConfig.useBlurhash
},
placeholderName() {
if (this.attachment.description === '' || !this.attachment.description) { if (this.attachment.description === '' || !this.attachment.description) {
return this.type.toUpperCase() return this.type.toUpperCase()
} }
return this.attachment.description return this.attachment.description
}, },
placeholderIconClass () { placeholderIconClass() {
if (this.type === 'image') return 'image' if (this.type === 'image') return 'image'
if (this.type === 'video') return 'video' if (this.type === 'video') return 'video'
if (this.type === 'audio') return 'music' if (this.type === 'audio') return 'music'
return 'file' return 'file'
}, },
referrerpolicy () { referrerpolicy() {
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer' return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
}, },
type () { type() {
return fileTypeService.fileType(this.attachment.mimetype) return fileTypeService.fileType(this.attachment.mimetype)
}, },
hidden () { hidden() {
return this.nsfw && this.hideNsfwLocal && !this.showHidden return this.nsfw && this.hideNsfwLocal && !this.showHidden
}, },
isEmpty () { isEmpty() {
return (this.type === 'html' && !this.attachment.oembed) return this.type === 'html' && !this.attachment.oembed
}, },
useModal () { useModal() {
let modalTypes = [] let modalTypes = []
switch (this.size) { switch (this.size) {
case 'hide': case 'hide':
@ -123,29 +130,29 @@ const Attachment = {
} }
return modalTypes.includes(this.type) return modalTypes.includes(this.type)
}, },
videoTag () { videoTag() {
return this.useModal ? 'button' : 'span' return this.useModal ? 'button' : 'span'
}, },
statusForm () { statusForm() {
return this.$parent.$parent return this.$parent.$parent
}, },
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
watch: { watch: {
'attachment.description' (newVal) { 'attachment.description'(newVal) {
this.localDescription = newVal this.localDescription = newVal
}, },
localDescription (newVal) { localDescription(newVal) {
this.onEdit(newVal) this.onEdit(newVal)
} }
}, },
methods: { methods: {
linkClicked ({ target }) { linkClicked({ target }) {
if (target.tagName === 'A') { if (target.tagName === 'A') {
window.open(target.href, '_blank') window.open(target.href, '_blank')
} }
}, },
openModal (event) { openModal(event) {
if (this.useModal) { if (this.useModal) {
this.$emit('setMedia') this.$emit('setMedia')
this.$store.dispatch('setCurrentMedia', this.attachment) this.$store.dispatch('setCurrentMedia', this.attachment)
@ -153,34 +160,35 @@ const Attachment = {
window.open(this.attachment.url) window.open(this.attachment.url)
} }
}, },
openModalForce (event) { openModalForce(event) {
this.$emit('setMedia') this.$emit('setMedia')
this.$store.dispatch('setCurrentMedia', this.attachment) this.$store.dispatch('setCurrentMedia', this.attachment)
}, },
onEdit (event) { onEdit(event) {
this.edit && this.edit(this.attachment, event) this.edit && this.edit(this.attachment, event)
}, },
onRemove () { onRemove() {
this.remove && this.remove(this.attachment) this.remove && this.remove(this.attachment)
}, },
onShiftUp () { onShiftUp() {
this.shiftUp && this.shiftUp(this.attachment) this.shiftUp && this.shiftUp(this.attachment)
}, },
onShiftDn () { onShiftDn() {
this.shiftDn && this.shiftDn(this.attachment) this.shiftDn && this.shiftDn(this.attachment)
}, },
stopFlash () { stopFlash() {
this.$refs.flash.closePlayer() this.$refs.flash.closePlayer()
}, },
setFlashLoaded (event) { setFlashLoaded(event) {
this.flashLoaded = event this.flashLoaded = event
}, },
toggleDescription () { toggleDescription() {
this.showDescription = !this.showDescription this.showDescription = !this.showDescription
}, },
toggleHidden (event) { toggleHidden(event) {
if ( if (
(this.mergedConfig.useOneClickNsfw && !this.showHidden) && this.mergedConfig.useOneClickNsfw &&
!this.showHidden &&
(this.type !== 'video' || this.mergedConfig.playVideosInModal) (this.type !== 'video' || this.mergedConfig.playVideosInModal)
) { ) {
this.openModal(event) this.openModal(event)
@ -201,14 +209,16 @@ const Attachment = {
this.showHidden = !this.showHidden this.showHidden = !this.showHidden
} }
}, },
onImageLoad (image) { onImageLoad(image) {
const width = image.naturalWidth const width = image.naturalWidth
const height = image.naturalHeight const height = image.naturalHeight
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height }) this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
}, },
resize (e) { resize(e) {
const target = e.target || e const target = e.target || e
if (!(target instanceof window.Element)) { return } if (!(target instanceof window.Element)) {
return
}
// Reset to default height for empty form, nothing else to do here. // Reset to default height for empty form, nothing else to do here.
if (target.value === '') { if (target.value === '') {
@ -219,14 +229,16 @@ const Attachment = {
const paddingString = getComputedStyle(target)['padding'] const paddingString = getComputedStyle(target)['padding']
// remove -px suffix // remove -px suffix
const padding = Number(paddingString.substring(0, paddingString.length - 2)) const padding = Number(
paddingString.substring(0, paddingString.length - 2)
)
target.style.height = 'auto' target.style.height = 'auto'
const newHeight = Math.floor(target.scrollHeight - padding * 2) const newHeight = Math.floor(target.scrollHeight - padding * 2)
target.style.height = `${newHeight}px` target.style.height = `${newHeight}px`
this.$emit('resize', newHeight) this.$emit('resize', newHeight)
}, },
postStatus (event) { postStatus(event) {
this.statusForm.postStatus(event, this.statusForm.newStatus) this.statusForm.postStatus(event, this.statusForm.newStatus)
} }
} }

View file

@ -117,7 +117,6 @@
padding-top: 0.5em; padding-top: 0.5em;
} }
.play-icon { .play-icon {
position: absolute; position: absolute;
font-size: 64px; font-size: 64px;

View file

@ -15,7 +15,8 @@
@click.prevent @click.prevent
> >
<FAIcon :icon="placeholderIconClass" /> <FAIcon :icon="placeholderIconClass" />
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }} <b>{{ nsfw ? 'NSFW / ' : '' }}</b
>{{ edit ? '' : placeholderName }}
</a> </a>
<div <div
v-if="edit || remove" v-if="edit || remove"
@ -30,7 +31,11 @@
</button> </button>
</div> </div>
<div <div
v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)" v-if="
size !== 'hide' &&
!hideDescription &&
(edit || localDescription || showDescription)
"
class="description-container" class="description-container"
:class="{ '-static': !edit }" :class="{ '-static': !edit }"
> >
@ -41,7 +46,7 @@
class="description-field" class="description-field"
:placeholder="$t('post_status.media_description')" :placeholder="$t('post_status.media_description')"
@keydown.enter.prevent="" @keydown.enter.prevent=""
> />
<p v-else> <p v-else>
{{ localDescription }} {{ localDescription }}
</p> </p>
@ -64,11 +69,19 @@
:title="attachment.description" :title="attachment.description"
@click.prevent.stop="toggleHidden" @click.prevent.stop="toggleHidden"
> >
<Blurhash
v-if="useBlurhash && attachment.blurhash"
:height="512"
:width="1024"
:hash="attachment.blurhash"
:punch="1"
/>
<img <img
v-else
:key="nsfwImage" :key="nsfwImage"
class="nsfw" class="nsfw"
:src="nsfwImage" :src="nsfwImage"
> />
<FAIcon <FAIcon
v-if="type === 'video'" v-if="type === 'video'"
class="play-icon" class="play-icon"
@ -88,7 +101,12 @@
<FAIcon icon="stop" /> <FAIcon icon="stop" />
</button> </button>
<button <button
v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'" v-if="
attachment.description &&
size !== 'small' &&
!edit &&
type !== 'unknown'
"
class="button-unstyled attachment-button" class="button-unstyled attachment-button"
:title="$t('status.show_attachment_description')" :title="$t('status.show_attachment_description')"
@click.prevent="toggleDescription" @click.prevent="toggleDescription"
@ -140,7 +158,7 @@
<a <a
v-if="type === 'image' && (!hidden || preloadImage)" v-if="type === 'image' && (!hidden || preloadImage)"
class="image-container" class="image-container"
:class="{'-hidden': hidden && preloadImage }" :class="{ '-hidden': hidden && preloadImage }"
:href="attachment.url" :href="attachment.url"
target="_blank" target="_blank"
@click.stop.prevent="openModal" @click.stop.prevent="openModal"
@ -218,11 +236,13 @@
v-if="attachment.thumb_url" v-if="attachment.thumb_url"
class="image" class="image"
> >
<img :src="attachment.thumb_url"> <img :src="attachment.thumb_url" />
</div> </div>
<div class="text"> <div class="text">
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1> <h1>
<a :href="attachment.url">{{ attachment.oembed.title }}</a>
</h1>
<div v-html="attachment.oembed.oembedHTML" /> <div v-html="attachment.oembed.oembedHTML" />
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
</div> </div>
@ -244,7 +264,11 @@
</span> </span>
</div> </div>
<div <div
v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))" v-if="
size !== 'hide' &&
!hideDescription &&
(edit || (localDescription && showDescription))
"
class="description-container" class="description-container"
:class="{ '-static': !edit }" :class="{ '-static': !edit }"
> >

View file

@ -6,13 +6,17 @@ import { mapGetters } from 'vuex'
const AuthForm = { const AuthForm = {
name: 'AuthForm', name: 'AuthForm',
render () { render() {
return h(resolveComponent(this.authForm)) return h(resolveComponent(this.authForm))
}, },
computed: { computed: {
authForm () { authForm() {
if (this.requiredTOTP) { return 'MFATOTPForm' } if (this.requiredTOTP) {
if (this.requiredRecovery) { return 'MFARecoveryForm' } return 'MFATOTPForm'
}
if (this.requiredRecovery) {
return 'MFARecoveryForm'
}
return 'LoginForm' return 'LoginForm'
}, },
...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery']) ...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])

View file

@ -2,11 +2,13 @@ const debounceMilliseconds = 500
export default { export default {
props: { props: {
query: { // function to query results and return a promise query: {
// function to query results and return a promise
type: Function, type: Function,
required: true required: true
}, },
filter: { // function to filter results in real time filter: {
// function to filter results in real time
type: Function type: Function
}, },
placeholder: { placeholder: {
@ -14,7 +16,7 @@ export default {
default: 'Search...' default: 'Search...'
} }
}, },
data () { data() {
return { return {
term: '', term: '',
timeout: null, timeout: null,
@ -23,29 +25,31 @@ export default {
} }
}, },
computed: { computed: {
filtered () { filtered() {
return this.filter ? this.filter(this.results) : this.results return this.filter ? this.filter(this.results) : this.results
} }
}, },
watch: { watch: {
term (val) { term(val) {
this.fetchResults(val) this.fetchResults(val)
} }
}, },
methods: { methods: {
fetchResults (term) { fetchResults(term) {
clearTimeout(this.timeout) clearTimeout(this.timeout)
this.timeout = setTimeout(() => { this.timeout = setTimeout(() => {
this.results = [] this.results = []
if (term) { if (term) {
this.query(term).then((results) => { this.results = results }) this.query(term).then((results) => {
this.results = results
})
} }
}, debounceMilliseconds) }, debounceMilliseconds)
}, },
onInputClick () { onInputClick() {
this.resultsVisible = true this.resultsVisible = true
}, },
onClickOutside () { onClickOutside() {
this.resultsVisible = false this.resultsVisible = false
} }
} }

View file

@ -8,7 +8,7 @@
:placeholder="placeholder" :placeholder="placeholder"
class="autosuggest-input" class="autosuggest-input"
@click="onInputClick" @click="onInputClick"
> />
<div <div
v-if="resultsVisible && filtered.length > 0" v-if="resultsVisible && filtered.length > 0"
class="autosuggest-results" class="autosuggest-results"

View file

@ -4,7 +4,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
const AvatarList = { const AvatarList = {
props: ['users'], props: ['users'],
computed: { computed: {
slicedUsers () { slicedUsers() {
return this.users ? this.users.slice(0, 15) : [] return this.users ? this.users.slice(0, 15) : []
} }
}, },
@ -12,8 +12,12 @@ const AvatarList = {
UserAvatar UserAvatar
}, },
methods: { methods: {
userProfileLink (user) { userProfileLink(user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(
user.id,
user.screen_name,
this.$store.state.instance.restrictedNicknames
)
} }
} }
} }

View file

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

View file

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

View file

@ -4,10 +4,8 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const BasicUserCard = { const BasicUserCard = {
props: [ props: ['user'],
'user' data() {
],
data () {
return { return {
userExpanded: false userExpanded: false
} }
@ -18,11 +16,15 @@ const BasicUserCard = {
RichContent RichContent
}, },
methods: { methods: {
toggleUserExpanded () { toggleUserExpanded() {
this.userExpanded = !this.userExpanded this.userExpanded = !this.userExpanded
}, },
userProfileLink (user) { userProfileLink(user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(
user.id,
user.screen_name,
this.$store.state.instance.restrictedNicknames
)
} }
} }
} }

View file

@ -2,19 +2,19 @@ import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const BlockCard = { const BlockCard = {
props: ['userId'], props: ['userId'],
data () { data() {
return { return {
progress: false progress: false
} }
}, },
computed: { computed: {
user () { user() {
return this.$store.getters.findUser(this.userId) return this.$store.getters.findUser(this.userId)
}, },
relationship () { relationship() {
return this.$store.getters.relationship(this.userId) return this.$store.getters.relationship(this.userId)
}, },
blocked () { blocked() {
return this.relationship.blocking return this.relationship.blocking
} }
}, },
@ -22,13 +22,13 @@ const BlockCard = {
BasicUserCard BasicUserCard
}, },
methods: { methods: {
unblockUser () { unblockUser() {
this.progress = true this.progress = true
this.$store.dispatch('unblockUser', this.user.id).then(() => { this.$store.dispatch('unblockUser', this.user.id).then(() => {
this.progress = false this.progress = false
}) })
}, },
blockUser () { blockUser() {
this.progress = true this.progress = true
this.$store.dispatch('blockUser', this.user.id).then(() => { this.$store.dispatch('blockUser', this.user.id).then(() => {
this.progress = false this.progress = false

View file

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

View file

@ -2,14 +2,14 @@ import Timeline from '../timeline/timeline.vue'
const Bookmarks = { const Bookmarks = {
computed: { computed: {
timeline () { timeline() {
return this.$store.state.statuses.timelines.bookmarks return this.$store.state.statuses.timelines.bookmarks
} }
}, },
components: { components: {
Timeline Timeline
}, },
unmounted () { unmounted() {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
} }
} }

View file

@ -4,15 +4,16 @@ const PublicTimeline = {
Timeline Timeline
}, },
computed: { computed: {
timeline () { return this.$store.state.statuses.timelines.bubble } timeline() {
return this.$store.state.statuses.timelines.bubble
}
}, },
created () { created() {
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' }) this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
}, },
unmounted () { unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'bubble') this.$store.dispatch('stopFetchingTimeline', 'bubble')
} }
} }
export default PublicTimeline export default PublicTimeline

View file

@ -11,14 +11,17 @@ export default {
name: 'Timeago', name: 'Timeago',
props: ['date'], props: ['date'],
computed: { computed: {
displayDate () { displayDate() {
const today = new Date() const today = new Date()
today.setHours(0, 0, 0, 0) today.setHours(0, 0, 0, 0)
if (this.date.getTime() === today.getTime()) { if (this.date.getTime() === today.getTime()) {
return this.$t('display_date.today') return this.$t('display_date.today')
} else { } else {
return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' }) return this.date.toLocaleDateString(
localeService.internalToBrowserLocale(this.$i18n.locale),
{ day: 'numeric', month: 'long' }
)
} }
} }
} }

View file

@ -9,7 +9,7 @@
:checked="modelValue" :checked="modelValue"
:indeterminate="indeterminate" :indeterminate="indeterminate"
@change="$emit('update:modelValue', $event.target.checked)" @change="$emit('update:modelValue', $event.target.checked)"
> />
<i class="checkbox-indicator" /> <i class="checkbox-indicator" />
<span <span
v-if="!!$slots.default" v-if="!!$slots.default"
@ -22,12 +22,8 @@
<script> <script>
export default { export default {
emits: ['update:modelValue'], props: ['modelValue', 'indeterminate', 'disabled'],
props: [ emits: ['update:modelValue']
'modelValue',
'indeterminate',
'disabled'
]
} }
</script> </script>
@ -56,7 +52,7 @@ export default {
border-radius: $fallback--checkboxRadius; border-radius: $fallback--checkboxRadius;
border-radius: var(--checkboxRadius, $fallback--checkboxRadius); border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
box-shadow: 0px 0px 2px black inset; box-shadow: 0px 0px 2px black inset;
box-shadow: var(--inputShadow); box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset;
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--input, $fallback--fg); background-color: var(--input, $fallback--fg);
vertical-align: top; vertical-align: top;
@ -71,7 +67,7 @@ export default {
&.disabled { &.disabled {
.checkbox-indicator::before, .checkbox-indicator::before,
.label { .label {
opacity: .5; opacity: 0.5;
} }
.label { .label {
color: $fallback--faint; color: $fallback--faint;
@ -79,7 +75,7 @@ export default {
} }
} }
input[type=checkbox] { input[type='checkbox'] {
display: none; display: none;
&:checked + .checkbox-indicator::before { &:checked + .checkbox-indicator::before {
@ -92,11 +88,10 @@ export default {
color: $fallback--text; color: $fallback--text;
color: var(--inputText, $fallback--text); color: var(--inputText, $fallback--text);
} }
} }
& > span { & > span {
margin-left: .5em; margin-left: 0.5em;
} }
} }
</style> </style>

View file

@ -8,7 +8,7 @@
flex: 0 0 0; flex: 0 0 0;
max-width: 9em; max-width: 9em;
align-items: stretch; align-items: stretch;
padding: .2em 8px; padding: 0.2em 8px;
input { input {
background: none; background: none;
@ -40,9 +40,10 @@
} }
.transparentIndicator { .transparentIndicator {
// forgot to install counter-strike source, ooops // forgot to install counter-strike source, ooops
background-color: #FF00FF; background-color: #ff00ff;
position: relative; position: relative;
&::before, &::after { &::before,
&::after {
display: block; display: block;
content: ''; content: '';
background-color: #000000; background-color: #000000;
@ -64,5 +65,4 @@
.label { .label {
flex: 1 1 auto; flex: 1 1 auto;
} }
} }

View file

@ -14,7 +14,12 @@
:model-value="present" :model-value="present"
:disabled="disabled" :disabled="disabled"
class="opt" class="opt"
@update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" @update:modelValue="
$emit(
'update:modelValue',
typeof modelValue === 'undefined' ? fallback : undefined
)
"
/> />
<div class="input color-input-field"> <div class="input color-input-field">
<input <input
@ -24,7 +29,7 @@
:value="modelValue || fallback" :value="modelValue || fallback"
:disabled="!present || disabled" :disabled="!present || disabled"
@input="$emit('update:modelValue', $event.target.value)" @input="$emit('update:modelValue', $event.target.value)"
> />
<input <input
v-if="validColor" v-if="validColor"
:id="name" :id="name"
@ -33,7 +38,7 @@
:value="modelValue || fallback" :value="modelValue || fallback"
:disabled="!present || disabled" :disabled="!present || disabled"
@input="$emit('update:modelValue', $event.target.value)" @input="$emit('update:modelValue', $event.target.value)"
> />
<div <div
v-if="transparentColor" v-if="transparentColor"
class="transparentIndicator" class="transparentIndicator"
@ -41,12 +46,11 @@
<div <div
v-if="computedColor" v-if="computedColor"
class="computedIndicator" class="computedIndicator"
:style="{backgroundColor: fallback}" :style="{ backgroundColor: fallback }"
/> />
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" src="./color_input.scss"></style>
<script> <script>
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js' import { hex2rgb } from '../../services/color_convert/color_convert.js'
@ -93,21 +97,22 @@ export default {
}, },
emits: ['update:modelValue'], emits: ['update:modelValue'],
computed: { computed: {
present () { present() {
return typeof this.modelValue !== 'undefined' return typeof this.modelValue !== 'undefined'
}, },
validColor () { validColor() {
return hex2rgb(this.modelValue || this.fallback) return hex2rgb(this.modelValue || this.fallback)
}, },
transparentColor () { transparentColor() {
return this.modelValue === 'transparent' return this.modelValue === 'transparent'
}, },
computedColor () { computedColor() {
return this.modelValue && this.modelValue.startsWith('--') return this.modelValue && this.modelValue.startsWith('--')
} }
} }
} }
</script> </script>
<style lang="scss" src="./color_input.scss"></style>
<style lang="scss"> <style lang="scss">
.color-control { .color-control {

View file

@ -22,13 +22,12 @@ const ConfirmModal = {
type: String type: String
} }
}, },
computed: { computed: {},
},
methods: { methods: {
onCancel () { onCancel() {
this.$emit('cancelled') this.$emit('cancelled')
}, },
onAccept () { onAccept() {
this.$emit('accepted') this.$emit('accepted')
} }
} }

View file

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

View file

@ -43,11 +43,7 @@ import {
faThumbsUp faThumbsUp
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faAdjust, faExclamationTriangle, faThumbsUp)
faAdjust,
faExclamationTriangle,
faThumbsUp
)
export default { export default {
props: { props: {
@ -65,19 +61,35 @@ export default {
} }
}, },
computed: { computed: {
hint () { hint() {
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad') const levelVal = this.contrast.aaa
? 'aaa'
: this.contrast.aa
? 'aa'
: 'bad'
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`) const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
const context = this.$t('settings.style.common.contrast.context.text') const context = this.$t('settings.style.common.contrast.context.text')
const ratio = this.contrast.text const ratio = this.contrast.text
return this.$t('settings.style.common.contrast.hint', { level, context, ratio }) return this.$t('settings.style.common.contrast.hint', {
level,
context,
ratio
})
}, },
hint_18pt () { hint_18pt() {
const levelVal = this.contrast.laaa ? 'aaa' : (this.contrast.laa ? 'aa' : 'bad') const levelVal = this.contrast.laaa
? 'aaa'
: this.contrast.laa
? 'aa'
: 'bad'
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`) const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
const context = this.$t('settings.style.common.contrast.context.18pt') const context = this.$t('settings.style.common.contrast.context.18pt')
const ratio = this.contrast.text const ratio = this.contrast.text
return this.$t('settings.style.common.contrast.hint', { level, context, ratio }) return this.$t('settings.style.common.contrast.hint', {
level,
context,
ratio
})
} }
} }
} }

View file

@ -5,7 +5,7 @@ const conversationPage = {
Conversation Conversation
}, },
computed: { computed: {
statusId () { statusId() {
return this.$route.params.id return this.$route.params.id
} }
} }

View file

@ -11,11 +11,7 @@ import {
faChevronLeft faChevronLeft
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faAngleDoubleDown, faAngleDoubleLeft, faChevronLeft)
faAngleDoubleDown,
faAngleDoubleLeft,
faChevronLeft
)
const sortById = (a, b) => { const sortById = (a, b) => {
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
@ -39,16 +35,17 @@ const sortAndFilterConversation = (conversation, statusoid) => {
if (statusoid.type === 'retweet') { if (statusoid.type === 'retweet') {
conversation = filter( conversation = filter(
conversation, conversation,
(status) => (status.type === 'retweet' || status.id !== statusoid.retweeted_status.id) (status) =>
status.type === 'retweet' || status.id !== statusoid.retweeted_status.id
) )
} else { } else {
conversation = filter(conversation, (status) => status.type !== 'retweet') conversation = filter(conversation, (status) => status.type !== 'retweet')
} }
return conversation.filter(_ => _).sort(sortById) return conversation.filter((_) => _).sort(sortById)
} }
const conversation = { const conversation = {
data () { data() {
return { return {
highlight: null, highlight: null,
expanded: false, expanded: false,
@ -66,74 +63,78 @@ const conversation = {
'profileUserId', 'profileUserId',
'virtualHidden' 'virtualHidden'
], ],
created () { created() {
if (this.isPage) { if (this.isPage) {
this.fetchConversation() this.fetchConversation()
} }
}, },
computed: { computed: {
maxDepthToShowByDefault () { maxDepthToShowByDefault() {
// maxDepthInThread = max number of depths that is *visible* // maxDepthInThread = max number of depths that is *visible*
// since our depth starts with 0 and "showing" means "showing children" // since our depth starts with 0 and "showing" means "showing children"
// there is a -2 here // there is a -2 here
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2 const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
return maxDepth >= 1 ? maxDepth : 1 return maxDepth >= 1 ? maxDepth : 1
}, },
streamingEnabled () { streamingEnabled() {
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED return (
this.mergedConfig.useStreamingApi &&
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
)
}, },
displayStyle () { displayStyle() {
return this.$store.getters.mergedConfig.conversationDisplay return this.$store.getters.mergedConfig.conversationDisplay
}, },
isTreeView () { isTreeView() {
return !this.isLinearView return !this.isLinearView
}, },
treeViewIsSimple () { treeViewIsSimple() {
return !this.$store.getters.mergedConfig.conversationTreeAdvanced return !this.$store.getters.mergedConfig.conversationTreeAdvanced
}, },
isLinearView () { isLinearView() {
return this.displayStyle === 'linear' return this.displayStyle === 'linear'
}, },
shouldFadeAncestors () { shouldFadeAncestors() {
return this.$store.getters.mergedConfig.conversationTreeFadeAncestors return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
}, },
otherRepliesButtonPosition () { otherRepliesButtonPosition() {
return this.$store.getters.mergedConfig.conversationOtherRepliesButton return this.$store.getters.mergedConfig.conversationOtherRepliesButton
}, },
showOtherRepliesButtonBelowStatus () { showOtherRepliesButtonBelowStatus() {
return this.otherRepliesButtonPosition === 'below' return this.otherRepliesButtonPosition === 'below'
}, },
showOtherRepliesButtonInsideStatus () { showOtherRepliesButtonInsideStatus() {
return this.otherRepliesButtonPosition === 'inside' return this.otherRepliesButtonPosition === 'inside'
}, },
suspendable () { suspendable() {
if (this.isTreeView) { if (this.isTreeView) {
return Object.entries(this.statusContentProperties) return Object.entries(this.statusContentProperties).every(
.every(([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0) ([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0
)
} }
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) { if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
return this.$refs.statusComponent.every(s => s.suspendable) return this.$refs.statusComponent.every((s) => s.suspendable)
} else { } else {
return true return true
} }
}, },
hideStatus () { hideStatus() {
return this.virtualHidden && this.suspendable return this.virtualHidden && this.suspendable
}, },
status () { status() {
return this.$store.state.statuses.allStatusesObject[this.statusId] return this.$store.state.statuses.allStatusesObject[this.statusId]
}, },
originalStatusId () { originalStatusId() {
if (this.status.retweeted_status) { if (this.status.retweeted_status) {
return this.status.retweeted_status.id return this.status.retweeted_status.id
} else { } else {
return this.statusId return this.statusId
} }
}, },
conversationId () { conversationId() {
return this.getConversationId(this.statusId) return this.getConversationId(this.statusId)
}, },
conversation () { conversation() {
if (!this.status) { if (!this.status) {
return [] return []
} }
@ -142,155 +143,203 @@ const conversation = {
return [this.status] return [this.status]
} }
const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId]) const conversation = clone(
const statusIndex = findIndex(conversation, { id: this.originalStatusId }) this.$store.state.statuses.conversationsObject[this.conversationId]
)
const statusIndex = findIndex(conversation, {
id: this.originalStatusId
})
if (statusIndex !== -1) { if (statusIndex !== -1) {
conversation[statusIndex] = this.status conversation[statusIndex] = this.status
} }
return sortAndFilterConversation(conversation, this.status) return sortAndFilterConversation(conversation, this.status)
}, },
statusMap () { statusMap() {
return this.conversation.reduce((res, s) => { return this.conversation.reduce((res, s) => {
res[s.id] = s res[s.id] = s
return res return res
}, {}) }, {})
}, },
threadTree () { threadTree() {
const reverseLookupTable = this.conversation.reduce((table, status, index) => { const reverseLookupTable = this.conversation.reduce(
table[status.id] = index (table, status, index) => {
return table table[status.id] = index
}, {}) return table
},
{}
)
const threads = this.conversation.reduce((a, cur) => { const threads = this.conversation.reduce(
const id = cur.id (a, cur) => {
a.forest[id] = this.getReplies(id) const id = cur.id
.map(s => s.id) a.forest[id] = this.getReplies(id).map((s) => s.id)
return a return a
}, { },
forest: {} {
}) forest: {}
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
if (processed[id]) {
return []
} }
)
processed[id] = true const walk = (forest, topLevel, depth = 0, processed = {}) =>
return [{ topLevel
status: this.conversation[reverseLookupTable[id]], .map((id) => {
id, if (processed[id]) {
depth return []
}, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), []) }
}).reduce((a, b) => a.concat(b), [])
const linearized = walk(threads.forest, this.topLevel.map(k => k.id)) processed[id] = true
return [
{
status: this.conversation[reverseLookupTable[id]],
id,
depth
},
walk(forest, forest[id], depth + 1, processed)
].reduce((a, b) => a.concat(b), [])
})
.reduce((a, b) => a.concat(b), [])
const linearized = walk(
threads.forest,
this.topLevel.map((k) => k.id)
)
return linearized return linearized
}, },
replyIds () { replyIds() {
return this.conversation.map(k => k.id) return this.conversation
.map((k) => k.id)
.reduce((res, id) => { .reduce((res, id) => {
res[id] = (this.replies[id] || []).map(k => k.id) res[id] = (this.replies[id] || []).map((k) => k.id)
return res return res
}, {}) }, {})
}, },
totalReplyCount () { totalReplyCount() {
const sizes = {} const sizes = {}
const subTreeSizeFor = (id) => { const subTreeSizeFor = (id) => {
if (sizes[id]) { if (sizes[id]) {
return sizes[id] return sizes[id]
} }
sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0) sizes[id] =
1 +
this.replyIds[id]
.map((cid) => subTreeSizeFor(cid))
.reduce((a, b) => a + b, 0)
return sizes[id] return sizes[id]
} }
this.conversation.map(k => k.id).map(subTreeSizeFor) this.conversation.map((k) => k.id).map(subTreeSizeFor)
return Object.keys(sizes).reduce((res, id) => { return Object.keys(sizes).reduce((res, id) => {
res[id] = sizes[id] - 1 // exclude itself res[id] = sizes[id] - 1 // exclude itself
return res return res
}, {}) }, {})
}, },
totalReplyDepth () { totalReplyDepth() {
const depths = {} const depths = {}
const subTreeDepthFor = (id) => { const subTreeDepthFor = (id) => {
if (depths[id]) { if (depths[id]) {
return depths[id] return depths[id]
} }
depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0) depths[id] =
1 +
this.replyIds[id]
.map((cid) => subTreeDepthFor(cid))
.reduce((a, b) => (a > b ? a : b), 0)
return depths[id] return depths[id]
} }
this.conversation.map(k => k.id).map(subTreeDepthFor) this.conversation.map((k) => k.id).map(subTreeDepthFor)
return Object.keys(depths).reduce((res, id) => { return Object.keys(depths).reduce((res, id) => {
res[id] = depths[id] - 1 // exclude itself res[id] = depths[id] - 1 // exclude itself
return res return res
}, {}) }, {})
}, },
depths () { depths() {
return this.threadTree.reduce((a, k) => { return this.threadTree.reduce((a, k) => {
a[k.id] = k.depth a[k.id] = k.depth
return a return a
}, {}) }, {})
}, },
topLevel () { topLevel() {
const topLevel = this.conversation.reduce((tl, cur) => const topLevel = this.conversation.reduce(
tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation) (tl, cur) =>
tl.filter(
(k) =>
this.getReplies(cur.id)
.map((v) => v.id)
.indexOf(k.id) === -1
),
this.conversation
)
return topLevel return topLevel
}, },
otherTopLevelCount () { otherTopLevelCount() {
return this.topLevel.length - 1 return this.topLevel.length - 1
}, },
showingTopLevel () { showingTopLevel() {
if (this.canDive && this.diveRoot) { if (this.canDive && this.diveRoot) {
return [this.statusMap[this.diveRoot]] return [this.statusMap[this.diveRoot]]
} }
return this.topLevel return this.topLevel
}, },
diveRoot () { diveRoot() {
const statusId = this.inlineDivePosition || this.statusId const statusId = this.inlineDivePosition || this.statusId
const isTopLevel = !this.parentOf(statusId) const isTopLevel = !this.parentOf(statusId)
return isTopLevel ? null : statusId return isTopLevel ? null : statusId
}, },
diveDepth () { diveDepth() {
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0 return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
}, },
diveMode () { diveMode() {
return this.canDive && !!this.diveRoot return this.canDive && !!this.diveRoot
}, },
shouldShowAllConversationButton () { shouldShowAllConversationButton() {
// The "show all conversation" button tells the user that there exist // The "show all conversation" button tells the user that there exist
// other toplevel statuses, so do not show it if there is only a single root // other toplevel statuses, so do not show it if there is only a single root
return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1 return (
this.isTreeView &&
this.isExpanded &&
this.diveMode &&
this.topLevel.length > 1
)
}, },
shouldShowAncestors () { shouldShowAncestors() {
return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length return (
this.isTreeView &&
this.isExpanded &&
this.ancestorsOf(this.diveRoot).length
)
}, },
replies () { replies() {
let i = 1 let i = 1
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => { return reduce(
/* eslint-disable camelcase */ this.conversation,
const irid = in_reply_to_status_id (result, { id, in_reply_to_status_id }) => {
/* eslint-enable camelcase */ /* eslint-disable camelcase */
if (irid) { const irid = in_reply_to_status_id
result[irid] = result[irid] || [] /* eslint-enable camelcase */
result[irid].push({ if (irid) {
name: `#${i}`, result[irid] = result[irid] || []
id: id result[irid].push({
}) name: `#${i}`,
} id: id
i++ })
return result }
}, {}) i++
return result
},
{}
)
}, },
isExpanded () { isExpanded() {
return !!(this.expanded || this.isPage) return !!(this.expanded || this.isPage)
}, },
hiddenStyle () { hiddenStyle() {
const height = (this.status && this.status.virtualHeight) || '120px' const height = (this.status && this.status.virtualHeight) || '120px'
return this.virtualHidden ? { height } : {} return this.virtualHidden ? { height } : {}
}, },
threadDisplayStatus () { threadDisplayStatus() {
return this.conversation.reduce((a, k) => { return this.conversation.reduce((a, k) => {
const id = k.id const id = k.id
const depth = this.depths[id] const depth = this.depths[id]
@ -298,7 +347,7 @@ const conversation = {
if (this.threadDisplayStatusObject[id]) { if (this.threadDisplayStatusObject[id]) {
return this.threadDisplayStatusObject[id] return this.threadDisplayStatusObject[id]
} }
if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) { if (depth - this.diveDepth <= this.maxDepthToShowByDefault) {
return 'showing' return 'showing'
} else { } else {
return 'hidden' return 'hidden'
@ -309,7 +358,7 @@ const conversation = {
return a return a
}, {}) }, {})
}, },
statusContentProperties () { statusContentProperties() {
return this.conversation.reduce((a, k) => { return this.conversation.reduce((a, k) => {
const id = k.id const id = k.id
const props = (() => { const props = (() => {
@ -334,20 +383,20 @@ const conversation = {
return a return a
}, {}) }, {})
}, },
canDive () { canDive() {
return this.isTreeView && this.isExpanded return this.isTreeView && this.isExpanded
}, },
focused () { focused() {
return (id) => { return (id) => {
return (this.isExpanded) && id === this.highlight return this.isExpanded && id === this.highlight
} }
}, },
maybeHighlight () { maybeHighlight() {
return this.isExpanded ? this.highlight : null return this.isExpanded ? this.highlight : null
}, },
...mapGetters(['mergedConfig']), ...mapGetters(['mergedConfig']),
...mapState({ ...mapState({
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus
}) })
}, },
components: { components: {
@ -355,53 +404,59 @@ const conversation = {
ThreadTree ThreadTree
}, },
watch: { watch: {
statusId (newVal, oldVal) { statusId(newVal, oldVal) {
const newConversationId = this.getConversationId(newVal) const newConversationId = this.getConversationId(newVal)
const oldConversationId = this.getConversationId(oldVal) const oldConversationId = this.getConversationId(oldVal)
if (newConversationId && oldConversationId && newConversationId === oldConversationId) { if (
newConversationId &&
oldConversationId &&
newConversationId === oldConversationId
) {
this.setHighlight(this.originalStatusId) this.setHighlight(this.originalStatusId)
} else { } else {
this.fetchConversation() this.fetchConversation()
} }
}, },
expanded (value) { expanded(value) {
if (value) { if (value) {
this.fetchConversation() this.fetchConversation()
} else { } else {
this.resetDisplayState() this.resetDisplayState()
} }
}, },
virtualHidden (value) { virtualHidden(value) {
this.$store.dispatch( this.$store.dispatch('setVirtualHeight', {
'setVirtualHeight', statusId: this.statusId,
{ statusId: this.statusId, height: `${this.$el.clientHeight}px` } height: `${this.$el.clientHeight}px`
) })
} }
}, },
methods: { methods: {
fetchConversation () { fetchConversation() {
if (this.status) { if (this.status) {
this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusId }) this.$store.state.api.backendInteractor
.fetchConversation({ id: this.statusId })
.then(({ ancestors, descendants }) => { .then(({ ancestors, descendants }) => {
this.$store.dispatch('addNewStatuses', { statuses: ancestors }) this.$store.dispatch('addNewStatuses', { statuses: ancestors })
this.$store.dispatch('addNewStatuses', { statuses: descendants }) this.$store.dispatch('addNewStatuses', { statuses: descendants })
this.setHighlight(this.originalStatusId) this.setHighlight(this.originalStatusId)
}) })
} else { } else {
this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId }) this.$store.state.api.backendInteractor
.fetchStatus({ id: this.statusId })
.then((status) => { .then((status) => {
this.$store.dispatch('addNewStatuses', { statuses: [status] }) this.$store.dispatch('addNewStatuses', { statuses: [status] })
this.fetchConversation() this.fetchConversation()
}) })
} }
}, },
getReplies (id) { getReplies(id) {
return this.replies[id] || [] return this.replies[id] || []
}, },
getHighlight () { getHighlight() {
return this.isExpanded ? this.highlight : null return this.isExpanded ? this.highlight : null
}, },
setHighlight (id) { setHighlight(id) {
if (!id) return if (!id) return
this.highlight = id this.highlight = id
@ -412,32 +467,38 @@ const conversation = {
this.$store.dispatch('fetchFavsAndRepeats', id) this.$store.dispatch('fetchFavsAndRepeats', id)
this.$store.dispatch('fetchEmojiReactionsBy', id) this.$store.dispatch('fetchEmojiReactionsBy', id)
}, },
toggleExpanded () { toggleExpanded() {
this.expanded = !this.expanded this.expanded = !this.expanded
}, },
getConversationId (statusId) { getConversationId(statusId) {
const status = this.$store.state.statuses.allStatusesObject[statusId] const status = this.$store.state.statuses.allStatusesObject[statusId]
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id')) return get(
status,
'retweeted_status.statusnet_conversation_id',
get(status, 'statusnet_conversation_id')
)
}, },
setThreadDisplay (id, nextStatus) { setThreadDisplay(id, nextStatus) {
this.threadDisplayStatusObject = { this.threadDisplayStatusObject = {
...this.threadDisplayStatusObject, ...this.threadDisplayStatusObject,
[id]: nextStatus [id]: nextStatus
} }
}, },
toggleThreadDisplay (id) { toggleThreadDisplay(id) {
const curStatus = this.threadDisplayStatus[id] const curStatus = this.threadDisplayStatus[id]
const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing' const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
this.setThreadDisplay(id, nextStatus) this.setThreadDisplay(id, nextStatus)
}, },
setThreadDisplayRecursively (id, nextStatus) { setThreadDisplayRecursively(id, nextStatus) {
this.setThreadDisplay(id, nextStatus) this.setThreadDisplay(id, nextStatus)
this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus)) this.getReplies(id)
.map((k) => k.id)
.map((id) => this.setThreadDisplayRecursively(id, nextStatus))
}, },
showThreadRecursively (id) { showThreadRecursively(id) {
this.setThreadDisplayRecursively(id, 'showing') this.setThreadDisplayRecursively(id, 'showing')
}, },
setStatusContentProperty (id, name, value) { setStatusContentProperty(id, name, value) {
this.statusContentPropertiesObject = { this.statusContentPropertiesObject = {
...this.statusContentPropertiesObject, ...this.statusContentPropertiesObject,
[id]: { [id]: {
@ -446,10 +507,14 @@ const conversation = {
} }
} }
}, },
toggleStatusContentProperty (id, name) { toggleStatusContentProperty(id, name) {
this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name]) this.setStatusContentProperty(
id,
name,
!this.statusContentProperties[id][name]
)
}, },
leastVisibleAncestor (id) { leastVisibleAncestor(id) {
let cur = id let cur = id
let parent = this.parentOf(cur) let parent = this.parentOf(cur)
while (cur) { while (cur) {
@ -463,18 +528,20 @@ const conversation = {
// nothing found, fall back to toplevel // nothing found, fall back to toplevel
return this.topLevel[0] ? this.topLevel[0].id : undefined return this.topLevel[0] ? this.topLevel[0].id : undefined
}, },
diveIntoStatus (id, preventScroll) { diveIntoStatus(id, preventScroll) {
this.tryScrollTo(id) this.tryScrollTo(id)
}, },
diveToTopLevel () { diveToTopLevel() {
this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id) this.tryScrollTo(
this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id
)
}, },
// only used when we are not on a page // only used when we are not on a page
undive () { undive() {
this.inlineDivePosition = null this.inlineDivePosition = null
this.setHighlight(this.statusId) this.setHighlight(this.statusId)
}, },
tryScrollTo (id) { tryScrollTo(id) {
if (!id) { if (!id) {
return return
} }
@ -503,13 +570,13 @@ const conversation = {
this.setHighlight(id) this.setHighlight(id)
}) })
}, },
goToCurrent () { goToCurrent() {
this.tryScrollTo(this.diveRoot || this.topLevel[0].id) this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
}, },
statusById (id) { statusById(id) {
return this.statusMap[id] return this.statusMap[id]
}, },
parentOf (id) { parentOf(id) {
const status = this.statusById(id) const status = this.statusById(id)
if (!status) { if (!status) {
return undefined return undefined
@ -520,11 +587,11 @@ const conversation = {
} }
return parentId return parentId
}, },
parentOrSelf (id) { parentOrSelf(id) {
return this.parentOf(id) || id return this.parentOf(id) || id
}, },
// Ancestors of some status, from top to bottom // Ancestors of some status, from top to bottom
ancestorsOf (id) { ancestorsOf(id) {
const ancestors = [] const ancestors = []
let cur = this.parentOf(id) let cur = this.parentOf(id)
while (cur) { while (cur) {
@ -533,7 +600,7 @@ const conversation = {
} }
return ancestors return ancestors
}, },
topLevelAncestorOrSelfId (id) { topLevelAncestorOrSelfId(id) {
let cur = id let cur = id
let parent = this.parentOf(id) let parent = this.parentOf(id)
while (parent) { while (parent) {
@ -542,7 +609,7 @@ const conversation = {
} }
return cur return cur
}, },
resetDisplayState () { resetDisplayState() {
this.undive() this.undive()
this.threadDisplayStatusObject = {} this.threadDisplayStatusObject = {}
} }

View file

@ -3,7 +3,7 @@
v-if="!hideStatus" v-if="!hideStatus"
:style="hiddenStyle" :style="hiddenStyle"
class="Conversation" class="Conversation"
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }" :class="{ '-expanded': isExpanded, panel: isExpanded }"
> >
<div <div
v-if="isExpanded" v-if="isExpanded"
@ -35,13 +35,15 @@
@click.prevent="diveToTopLevel" @click.prevent="diveToTopLevel"
> >
<template #icon> <template #icon>
<FAIcon <FAIcon icon="angle-double-left" />
icon="angle-double-left"
/>
</template> </template>
<template #text> <template #text>
<span> <span>
{{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }} {{
$tc('status.show_all_conversation', otherTopLevelCount, {
numStatus: otherTopLevelCount
})
}}
</span> </span>
</template> </template>
</i18n-t> </i18n-t>
@ -54,14 +56,20 @@
v-for="status in ancestorsOf(diveRoot)" v-for="status in ancestorsOf(diveRoot)"
:key="status.id" :key="status.id"
class="thread-ancestor" class="thread-ancestor"
:class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}" :class="{
'thread-ancestor-has-other-replies':
getReplies(status.id).length > 1,
'-faded': shouldFadeAncestors
}"
> >
<status <status
ref="statusComponent" ref="statusComponent"
:inline-expanded="collapsable && isExpanded" :inline-expanded="collapsable && isExpanded"
:statusoid="status" :statusoid="status"
:expandable="!isExpanded" :expandable="!isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]" :show-pinned="
pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]
"
:focused="focused(status.id)" :focused="focused(status.id)"
:in-conversation="isExpanded" :in-conversation="isExpanded"
:highlight="getHighlight()" :highlight="getHighlight()"
@ -69,7 +77,6 @@
:in-profile="inProfile" :in-profile="inProfile"
:profile-user-id="profileUserId" :profile-user-id="profileUserId"
class="conversation-status status-fadein panel-body" class="conversation-status status-fadein panel-body"
:simple-tree="treeViewIsSimple" :simple-tree="treeViewIsSimple"
:toggle-thread-display="toggleThreadDisplay" :toggle-thread-display="toggleThreadDisplay"
:thread-display-status="threadDisplayStatus" :thread-display-status="threadDisplayStatus"
@ -78,28 +85,47 @@
:total-reply-depth="totalReplyDepth" :total-reply-depth="totalReplyDepth"
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus" :show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
:dive="() => diveIntoStatus(status.id)" :dive="() => diveIntoStatus(status.id)"
:controlled-showing-tall="
:controlled-showing-tall="statusContentProperties[status.id].showingTall" statusContentProperties[status.id].showingTall
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject" "
:controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject" :controlled-expanding-subject="
statusContentProperties[status.id].expandingSubject
"
:controlled-showing-long-subject="
statusContentProperties[status.id].showingLongSubject
"
:controlled-replying="statusContentProperties[status.id].replying" :controlled-replying="statusContentProperties[status.id].replying"
:controlled-media-playing="statusContentProperties[status.id].mediaPlaying" :controlled-media-playing="
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')" statusContentProperties[status.id].mediaPlaying
:controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')" "
:controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')" :controlled-toggle-showing-tall="
:controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')" () => toggleStatusContentProperty(status.id, 'showingTall')
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)" "
:controlled-toggle-expanding-subject="
() => toggleStatusContentProperty(status.id, 'expandingSubject')
"
:controlled-toggle-showing-long-subject="
() =>
toggleStatusContentProperty(status.id, 'showingLongSubject')
"
:controlled-toggle-replying="
() => toggleStatusContentProperty(status.id, 'replying')
"
:controlled-set-media-playing="
(newVal) =>
toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)
"
@goto="setHighlight" @goto="setHighlight"
@toggleExpanded="toggleExpanded" @toggleExpanded="toggleExpanded"
/> />
<div <div
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1" v-if="
showOtherRepliesButtonBelowStatus &&
getReplies(status.id).length > 1
"
class="thread-ancestor-dive-box" class="thread-ancestor-dive-box"
> >
<div <div class="thread-ancestor-dive-box-inner">
class="thread-ancestor-dive-box-inner"
>
<i18n-t <i18n-t
tag="button" tag="button"
scope="global" scope="global"
@ -108,13 +134,17 @@
@click.prevent="diveIntoStatus(status.id)" @click.prevent="diveIntoStatus(status.id)"
> >
<template #icon> <template #icon>
<FAIcon <FAIcon icon="angle-double-right" />
icon="angle-double-right"
/>
</template> </template>
<template #text> <template #text>
<span> <span>
{{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }} {{
$tc(
'status.ancestor_follow',
getReplies(status.id).length - 1,
{ numReplies: getReplies(status.id).length - 1 }
)
}}
</span> </span>
</template> </template>
</i18n-t> </i18n-t>
@ -127,7 +157,6 @@
:key="status.id" :key="status.id"
ref="statusComponent" ref="statusComponent"
:depth="0" :depth="0"
:status="status" :status="status"
:in-profile="inProfile" :in-profile="inProfile"
:conversation="conversation" :conversation="conversation"
@ -135,13 +164,11 @@
:is-expanded="isExpanded" :is-expanded="isExpanded"
:pinned-status-ids-object="pinnedStatusIdsObject" :pinned-status-ids-object="pinnedStatusIdsObject"
:profile-user-id="profileUserId" :profile-user-id="profileUserId"
:focused="focused" :focused="focused"
:get-replies="getReplies" :get-replies="getReplies"
:highlight="maybeHighlight" :highlight="maybeHighlight"
:set-highlight="setHighlight" :set-highlight="setHighlight"
:toggle-expanded="toggleExpanded" :toggle-expanded="toggleExpanded"
:simple="treeViewIsSimple" :simple="treeViewIsSimple"
:toggle-thread-display="toggleThreadDisplay" :toggle-thread-display="toggleThreadDisplay"
:thread-display-status="threadDisplayStatus" :thread-display-status="threadDisplayStatus"
@ -165,7 +192,9 @@
:inline-expanded="collapsable && isExpanded" :inline-expanded="collapsable && isExpanded"
:statusoid="status" :statusoid="status"
:expandable="!isExpanded" :expandable="!isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]" :show-pinned="
pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]
"
:focused="focused(status.id)" :focused="focused(status.id)"
:in-conversation="isExpanded" :in-conversation="isExpanded"
:highlight="getHighlight()" :highlight="getHighlight()"
@ -173,7 +202,6 @@
:in-profile="inProfile" :in-profile="inProfile"
:profile-user-id="profileUserId" :profile-user-id="profileUserId"
class="conversation-status status-fadein panel-body" class="conversation-status status-fadein panel-body"
:toggle-thread-display="toggleThreadDisplay" :toggle-thread-display="toggleThreadDisplay"
:thread-display-status="threadDisplayStatus" :thread-display-status="threadDisplayStatus"
:show-thread-recursively="showThreadRecursively" :show-thread-recursively="showThreadRecursively"
@ -182,7 +210,6 @@
:status-content-properties="statusContentProperties" :status-content-properties="statusContentProperties"
:set-status-content-property="setStatusContentProperty" :set-status-content-property="setStatusContentProperty"
:toggle-status-content-property="toggleStatusContentProperty" :toggle-status-content-property="toggleStatusContentProperty"
@goto="setHighlight" @goto="setHighlight"
@toggleExpanded="toggleExpanded" @toggleExpanded="toggleExpanded"
/> />
@ -233,7 +260,8 @@
border-bottom-color: var(--border, $fallback--border); border-bottom-color: var(--border, $fallback--border);
border-radius: 0; border-radius: 0;
/* Make the button stretch along the whole row */ /* Make the button stretch along the whole row */
&, &-inner { &,
&-inner {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
flex-direction: column; flex-direction: column;
@ -253,8 +281,7 @@
.thread-ancestor-has-other-replies .conversation-status, .thread-ancestor-has-other-replies .conversation-status,
.thread-ancestor:last-child .conversation-status, .thread-ancestor:last-child .conversation-status,
.thread-ancestor:last-child .thread-ancestor-dive-box, .thread-ancestor:last-child .thread-ancestor-dive-box,
&:last-child .conversation-status, &:last-child .conversation-status {
&.-expanded .thread-tree .conversation-status {
border-bottom: none; border-bottom: none;
} }
@ -271,7 +298,8 @@
border-left-color: $fallback--cRed; border-left-color: $fallback--cRed;
border-left-color: var(--cRed, $fallback--cRed); border-left-color: var(--cRed, $fallback--cRed);
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); border-radius: 0 0 var(--panelRadius, $fallback--panelRadius)
var(--panelRadius, $fallback--panelRadius);
border-bottom: 1px solid var(--border, $fallback--border); border-bottom: 1px solid var(--border, $fallback--border);
} }

View file

@ -1,6 +1,11 @@
import SearchBar from 'components/search_bar/search_bar.vue' import SearchBar from 'components/search_bar/search_bar.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue' import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import {
publicTimelineVisible,
federatedTimelineVisible,
bubbleTimelineVisible
} from '../../lib/timeline_visibility'
import { import {
faSignInAlt, faSignInAlt,
faSignOutAlt, faSignOutAlt,
@ -19,6 +24,7 @@ import {
faInfoCircle, faInfoCircle,
faUserTie faUserTie
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { mapState } from 'vuex'
library.add( library.add(
faSignInAlt, faSignInAlt,
@ -46,76 +52,103 @@ export default {
}, },
data: () => ({ data: () => ({
searchBarHidden: true, searchBarHidden: true,
supportsMask: window.CSS && window.CSS.supports && ( supportsMask:
window.CSS.supports('mask-size', 'contain') || window.CSS &&
window.CSS.supports &&
(window.CSS.supports('mask-size', 'contain') ||
window.CSS.supports('-webkit-mask-size', 'contain') || window.CSS.supports('-webkit-mask-size', 'contain') ||
window.CSS.supports('-moz-mask-size', 'contain') || window.CSS.supports('-moz-mask-size', 'contain') ||
window.CSS.supports('-ms-mask-size', 'contain') || window.CSS.supports('-ms-mask-size', 'contain') ||
window.CSS.supports('-o-mask-size', 'contain') window.CSS.supports('-o-mask-size', 'contain')),
),
showingConfirmLogout: false showingConfirmLogout: false
}), }),
computed: { computed: {
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask }, enableMask() {
logoStyle () { return this.supportsMask && this.$store.state.instance.logoMask
},
logoStyle() {
return { return {
'visibility': this.enableMask ? 'hidden' : 'visible' visibility: this.enableMask ? 'hidden' : 'visible'
} }
}, },
logoMaskStyle () { logoMaskStyle() {
return this.enableMask ? { return this.enableMask
'mask-image': `url(${this.$store.state.instance.logo})` ? {
} : { 'mask-image': `url(${this.$store.state.instance.logo})`
'background-color': this.enableMask ? '' : 'transparent' }
} : {
'background-color': this.enableMask ? '' : 'transparent'
}
}, },
logoBgStyle () { logoBgStyle() {
return Object.assign({ return Object.assign(
'margin': `${this.$store.state.instance.logoMargin} 0`, {
opacity: this.searchBarHidden ? 1 : 0 margin: `${this.$store.state.instance.logoMargin} 0`,
}, this.enableMask ? {} : { opacity: this.searchBarHidden ? 1 : 0
'background-color': this.enableMask ? '' : 'transparent' },
}) this.enableMask
? {}
: {
'background-color': this.enableMask ? '' : 'transparent'
}
)
}, },
logo () { return this.$store.state.instance.logo }, logo() {
mergedConfig () { return this.$store.state.instance.logo
},
mergedConfig() {
return this.$store.getters.mergedConfig return this.$store.getters.mergedConfig
}, },
sitename () { return this.$store.state.instance.name }, sitename() {
showNavShortcuts () { return this.$store.state.instance.name
},
showNavShortcuts() {
return this.mergedConfig.showNavShortcuts return this.mergedConfig.showNavShortcuts
}, },
showWiderShortcuts () { showWiderShortcuts() {
return this.mergedConfig.showWiderShortcuts return this.mergedConfig.showWiderShortcuts
}, },
hideSiteFavicon () { hideSiteFavicon() {
return this.mergedConfig.hideSiteFavicon return this.mergedConfig.hideSiteFavicon
}, },
hideSiteName () { hideSiteName() {
return this.mergedConfig.hideSiteName return this.mergedConfig.hideSiteName
}, },
hideSitename () { return this.$store.state.instance.hideSitename }, hideSitename() {
logoLeft () { return this.$store.state.instance.logoLeft }, return this.$store.state.instance.hideSitename
currentUser () { return this.$store.state.users.currentUser }, },
privateMode () { return this.$store.state.instance.private }, logoLeft() {
shouldConfirmLogout () { return this.$store.state.instance.logoLeft
},
currentUser() {
return this.$store.state.users.currentUser
},
privateMode() {
return this.$store.state.instance.private
},
shouldConfirmLogout() {
return this.$store.getters.mergedConfig.modalOnLogout return this.$store.getters.mergedConfig.modalOnLogout
}, },
showBubbleTimeline () { showBubbleTimeline() {
return this.$store.state.instance.localBubbleInstances.length > 0 return this.$store.state.instance.localBubbleInstances.length > 0
} },
...mapState({
publicTimelineVisible,
federatedTimelineVisible,
bubbleTimelineVisible
})
}, },
methods: { methods: {
scrollToTop () { scrollToTop() {
window.scrollTo(0, 0) window.scrollTo(0, 0)
}, },
onSearchBarToggled (hidden) { onSearchBarToggled(hidden) {
this.searchBarHidden = hidden this.searchBarHidden = hidden
}, },
openSettingsModal () { openSettingsModal() {
this.$store.dispatch('openSettingsModal') this.$store.dispatch('openSettingsModal')
}, },
openModModal () { openModModal() {
this.$store.dispatch('openModModal') this.$store.dispatch('openModModal')
} }
} }

View file

@ -15,7 +15,7 @@
display: grid; display: grid;
grid-template-rows: var(--navbar-height); grid-template-rows: var(--navbar-height);
grid-template-columns: 2fr auto 2fr; grid-template-columns: 2fr auto 2fr;
grid-template-areas: "nav-left logo actions"; grid-template-areas: 'nav-left logo actions';
box-sizing: border-box; box-sizing: border-box;
padding: 0 1.2em; padding: 0 1.2em;
margin: auto; margin: auto;
@ -24,11 +24,12 @@
&.-logoLeft .inner-nav { &.-logoLeft .inner-nav {
grid-template-columns: auto 2fr 2fr; grid-template-columns: auto 2fr 2fr;
grid-template-areas: "logo nav-left actions"; grid-template-areas: 'logo nav-left actions';
} }
.button-default { .button-default {
&, svg { &,
svg {
color: $fallback--text; color: $fallback--text;
color: var(--btnTopBarText, $fallback--text); color: var(--btnTopBarText, $fallback--text);
} }
@ -49,7 +50,7 @@
color: $fallback--text; color: $fallback--text;
color: var(--btnToggledTopBarText, $fallback--text); color: var(--btnToggledTopBarText, $fallback--text);
background-color: $fallback--fg; background-color: $fallback--fg;
background-color: var(--btnToggledTopBar, $fallback--fg) background-color: var(--btnToggledTopBar, $fallback--fg);
} }
} }
@ -88,13 +89,25 @@
width: 2em; width: 2em;
height: 100%; height: 100%;
text-align: center; text-align: center;
display: inline-block;
&.router-link-active { &.router-link-active {
font-size: 1.2em; // box-shadow: 0 -2px 0 var(--selectedMenuText, $fallback--text) inset;
margin-top: 0.05em; position: relative;
&:after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 4px;
background-color: $fallback--fg;
background-color: var(--btn, $fallback--fg);
border-radius: $fallback--btnRadius;
border-radius: var(--btnRadius, $fallback--btnRadius);
}
.svg-inline--fa { .svg-inline--fa {
font-weight: bolder;
color: $fallback--text; color: $fallback--text;
color: var(--selectedMenuText, $fallback--text); color: var(--selectedMenuText, $fallback--text);
--lightText: var(--selectedMenuLightText, $fallback--lightText); --lightText: var(--selectedMenuLightText, $fallback--lightText);

View file

@ -18,8 +18,8 @@
<img <img
v-if="!hideSiteFavicon" v-if="!hideSiteFavicon"
class="favicon" class="favicon"
src="/favicon.png" src="/favicon.svg"
> />
<span <span
v-if="!hideSiteName" v-if="!hideSiteName"
class="site-name" class="site-name"
@ -44,6 +44,7 @@
/> />
</router-link> </router-link>
<router-link <router-link
v-if="publicTimelineVisible"
:to="{ name: 'public-timeline' }" :to="{ name: 'public-timeline' }"
class="nav-icon" class="nav-icon"
> >
@ -55,7 +56,7 @@
/> />
</router-link> </router-link>
<router-link <router-link
v-if="currentUser && showBubbleTimeline" v-if="bubbleTimelineVisible"
:to="{ name: 'bubble-timeline' }" :to="{ name: 'bubble-timeline' }"
class="nav-icon" class="nav-icon"
> >
@ -67,6 +68,7 @@
/> />
</router-link> </router-link>
<router-link <router-link
v-if="federatedTimelineVisible"
:to="{ name: 'public-external-timeline' }" :to="{ name: 'public-external-timeline' }"
class="nav-icon" class="nav-icon"
> >
@ -91,7 +93,7 @@
<img <img
:src="logo" :src="logo"
:style="logoStyle" :style="logoStyle"
> />
</router-link> </router-link>
<div class="item right actions"> <div class="item right actions">
<search-bar <search-bar
@ -106,7 +108,10 @@
<router-link <router-link
v-if="currentUser" v-if="currentUser"
class="nav-icon" class="nav-icon"
:to="{ name: 'interactions', params: { username: currentUser.screen_name } }" :to="{
name: 'interactions',
params: { username: currentUser.screen_name }
}"
> >
<FAIcon <FAIcon
fixed-width fixed-width
@ -152,7 +157,10 @@
/> />
</button> </button>
<button <button
v-if="currentUser && currentUser.role === 'admin' || currentUser.role === 'moderator'" v-if="
(currentUser && currentUser.role === 'admin') ||
currentUser.role === 'moderator'
"
class="button-unstyled nav-icon" class="button-unstyled nav-icon"
@click.stop="openModModal" @click.stop="openModModal"
> >

View file

@ -31,14 +31,14 @@
.dark-overlay { .dark-overlay {
&::before { &::before {
bottom: 0; bottom: 0;
content: " "; content: ' ';
display: block; display: block;
cursor: default; cursor: default;
left: 0; left: 0;
position: fixed; position: fixed;
right: 0; right: 0;
top: 0; top: 0;
background: rgba(27,31,35,.5); background: rgba(27, 31, 35, 0.5);
z-index: 2000; z-index: 2000;
} }
} }
@ -74,7 +74,7 @@
.dialog-modal-footer { .dialog-modal-footer {
margin: 0; margin: 0;
padding: .5em .5em; padding: 0.5em 0.5em;
background-color: $fallback--bg; background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg); background-color: var(--bg, $fallback--bg);
border-top: 1px solid $fallback--border; border-top: 1px solid $fallback--border;
@ -84,9 +84,8 @@
button { button {
width: auto; width: auto;
margin-left: .5rem; margin-left: 0.5rem;
} }
} }
} }
</style> </style>

View file

@ -2,7 +2,7 @@ import Timeline from '../timeline/timeline.vue'
const DMs = { const DMs = {
computed: { computed: {
timeline () { timeline() {
return this.$store.state.statuses.timelines.dms return this.$store.state.statuses.timelines.dms
} }
}, },

View file

@ -6,18 +6,18 @@ const DomainMuteCard = {
ProgressButton ProgressButton
}, },
computed: { computed: {
user () { user() {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
}, },
muted () { muted() {
return this.user.domainMutes.includes(this.domain) return this.user.domainMutes.includes(this.domain)
} }
}, },
methods: { methods: {
unmuteDomain () { unmuteDomain() {
return this.$store.dispatch('unmuteDomain', this.domain) return this.$store.dispatch('unmuteDomain', this.domain)
}, },
muteDomain () { muteDomain() {
return this.$store.dispatch('muteDomain', this.domain) return this.$store.dispatch('muteDomain', this.domain)
} }
} }

View file

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

View file

@ -8,27 +8,27 @@ const EditStatusModal = {
PostStatusForm, PostStatusForm,
Modal Modal
}, },
data () { data() {
return { return {
resettingForm: false resettingForm: false
} }
}, },
computed: { computed: {
isLoggedIn () { isLoggedIn() {
return !!this.$store.state.users.currentUser return !!this.$store.state.users.currentUser
}, },
modalActivated () { modalActivated() {
return this.$store.state.editStatus.modalActivated return this.$store.state.editStatus.modalActivated
}, },
isFormVisible () { isFormVisible() {
return this.isLoggedIn && !this.resettingForm && this.modalActivated return this.isLoggedIn && !this.resettingForm && this.modalActivated
}, },
params () { params() {
return this.$store.state.editStatus.params || {} return this.$store.state.editStatus.params || {}
} }
}, },
watch: { watch: {
params (newVal, oldVal) { params(newVal, oldVal) {
if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) { if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
this.resettingForm = true this.resettingForm = true
this.$nextTick(() => { this.$nextTick(() => {
@ -36,14 +36,16 @@ const EditStatusModal = {
}) })
} }
}, },
isFormVisible (val) { isFormVisible(val) {
if (val) { if (val) {
this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus()) this.$nextTick(
() => this.$el && this.$el.querySelector('textarea').focus()
)
} }
} }
}, },
methods: { methods: {
doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) { doEditStatus({ status, spoilerText, sensitive, media, contentType, poll }) {
const params = { const params = {
store: this.$store, store: this.$store,
statusId: this.$store.state.editStatus.params.statusId, statusId: this.$store.state.editStatus.params.statusId,
@ -55,7 +57,8 @@ const EditStatusModal = {
contentType contentType
} }
return statusPosterService.editStatus(params) return statusPosterService
.editStatus(params)
.then((data) => { .then((data) => {
return data return data
}) })
@ -66,7 +69,7 @@ const EditStatusModal = {
} }
}) })
}, },
closeModal () { closeModal() {
this.$store.dispatch('closeEditStatusModal') this.$store.dispatch('closeEditStatusModal')
} }
} }

View file

@ -11,10 +11,10 @@
<PostStatusForm <PostStatusForm
class="panel-body" class="panel-body"
v-bind="params" v-bind="params"
@posted="closeModal" :disable-polls="true"
:disablePolls="true" :disable-visibility-selector="true"
:disableVisibilitySelector="true"
:post-handler="doEditStatus" :post-handler="doEditStatus"
@posted="closeModal"
/> />
</div> </div>
</Modal> </Modal>

View file

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

View file

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

View file

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

View file

@ -4,13 +4,9 @@ import { take } from 'lodash'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
faSmileBeam
} from '@fortawesome/free-regular-svg-icons'
library.add( library.add(faSmileBeam)
faSmileBeam
)
/** /**
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs * EmojiInput - augmented inputs for emoji and autocomplete support in inputs
@ -105,7 +101,7 @@ const EmojiInput = {
default: false default: false
} }
}, },
data () { data() {
return { return {
input: undefined, input: undefined,
highlighted: 0, highlighted: 0,
@ -123,29 +119,34 @@ const EmojiInput = {
EmojiPicker EmojiPicker
}, },
computed: { computed: {
padEmoji () { padEmoji() {
return this.$store.getters.mergedConfig.padEmoji return this.$store.getters.mergedConfig.padEmoji
}, },
showSuggestions () { showSuggestions() {
return this.focused && return (
this.focused &&
this.suggestions && this.suggestions &&
this.suggestions.length > 0 && this.suggestions.length > 0 &&
!this.showPicker && !this.showPicker &&
!this.temporarilyHideSuggestions !this.temporarilyHideSuggestions
)
}, },
textAtCaret () { textAtCaret() {
return (this.wordAtCaret || {}).word || '' return (this.wordAtCaret || {}).word || ''
}, },
wordAtCaret () { wordAtCaret() {
if (this.modelValue && this.caret) { if (this.modelValue && this.caret) {
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {} const word =
Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
return word return word
} }
} }
}, },
mounted () { mounted() {
const { root } = this.$refs const { root } = this.$refs
const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea') const input =
root.querySelector('.emoji-input > input') ||
root.querySelector('.emoji-input > textarea')
if (!input) return if (!input) return
this.input = input this.input = input
this.resize() this.resize()
@ -158,7 +159,7 @@ const EmojiInput = {
input.addEventListener('transitionend', this.onTransition) input.addEventListener('transitionend', this.onTransition)
input.addEventListener('input', this.onInput) input.addEventListener('input', this.onInput)
}, },
unmounted () { unmounted() {
const { input } = this const { input } = this
if (input) { if (input) {
input.removeEventListener('blur', this.onBlur) input.removeEventListener('blur', this.onBlur)
@ -183,29 +184,29 @@ const EmojiInput = {
// Async: cancel if textAtCaret has changed during wait // Async: cancel if textAtCaret has changed during wait
if (this.textAtCaret !== newWord) return if (this.textAtCaret !== newWord) return
if (matchedSuggestions.length <= 0) return if (matchedSuggestions.length <= 0) return
this.suggestions = take(matchedSuggestions, 5) this.suggestions = take(matchedSuggestions, 5).map(
.map(({ imageUrl, ...rest }) => ({ ({ imageUrl, ...rest }) => ({
...rest, ...rest,
img: imageUrl || '' img: imageUrl || ''
})) })
)
}, },
suggestions: { suggestions: {
handler (newValue) { handler(newValue) {
this.$nextTick(this.resize) this.$nextTick(this.resize)
}, },
deep: true deep: true
} }
}, },
methods: { methods: {
focusPickerInput () { focusPickerInput() {
const pickerEl = this.$refs.picker.$el const pickerEl = this.$refs.picker.$el
if (!pickerEl) return if (!pickerEl) return
const pickerInput = pickerEl.querySelector('input') const pickerInput = pickerEl.querySelector('input')
if (pickerInput) pickerInput.focus() if (pickerInput) pickerInput.focus()
}, },
triggerShowPicker () { triggerShowPicker() {
this.showPicker = true this.showPicker = true
this.$refs.picker.startEmojiLoad()
this.$nextTick(() => { this.$nextTick(() => {
this.scrollIntoView() this.scrollIntoView()
this.focusPickerInput() this.focusPickerInput()
@ -218,21 +219,24 @@ const EmojiInput = {
this.disableClickOutside = false this.disableClickOutside = false
}, 0) }, 0)
}, },
togglePicker () { togglePicker() {
this.input.focus() this.input.focus()
this.showPicker = !this.showPicker this.showPicker = !this.showPicker
if (this.showPicker) { if (this.showPicker) {
this.scrollIntoView() this.scrollIntoView()
this.$refs.picker.startEmojiLoad()
this.$nextTick(this.focusPickerInput) this.$nextTick(this.focusPickerInput)
} }
}, },
replace (replacement) { replace(replacement) {
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement) const newValue = Completion.replaceWord(
this.modelValue,
this.wordAtCaret,
replacement
)
this.$emit('update:modelValue', newValue) this.$emit('update:modelValue', newValue)
this.caret = 0 this.caret = 0
}, },
insert ({ insertion, keepOpen, surroundingSpace = true }) { insert({ insertion, keepOpen, surroundingSpace = true }) {
const before = this.modelValue.substring(0, this.caret) || '' const before = this.modelValue.substring(0, this.caret) || ''
const after = this.modelValue.substring(this.caret) || '' const after = this.modelValue.substring(this.caret) || ''
@ -251,19 +255,25 @@ const EmojiInput = {
* them, masto seem to be rendering :emoji::emoji: correctly now so why not * them, masto seem to be rendering :emoji::emoji: correctly now so why not
*/ */
const isSpaceRegex = /\s/ const isSpaceRegex = /\s/
const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : '' const spaceBefore =
const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : '' surroundingSpace &&
!isSpaceRegex.exec(before.slice(-1)) &&
before.length &&
this.padEmoji > 0
? ' '
: ''
const spaceAfter =
surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji
? ' '
: ''
const newValue = [ const newValue = [before, spaceBefore, insertion, spaceAfter, after].join(
before, ''
spaceBefore, )
insertion,
spaceAfter,
after
].join('')
this.keepOpen = keepOpen this.keepOpen = keepOpen
this.$emit('update:modelValue', newValue) this.$emit('update:modelValue', newValue)
const position = this.caret + (insertion + spaceAfter + spaceBefore).length const position =
this.caret + (insertion + spaceAfter + spaceBefore).length
if (!keepOpen) { if (!keepOpen) {
this.input.focus() this.input.focus()
} }
@ -275,12 +285,17 @@ const EmojiInput = {
this.caret = position this.caret = position
}) })
}, },
replaceText (e, suggestion) { replaceText(e, suggestion) {
const len = this.suggestions.length || 0 const len = this.suggestions.length || 0
if (len > 0 || suggestion) { if (len > 0 || suggestion) {
const chosenSuggestion = suggestion || this.suggestions[this.highlighted] const chosenSuggestion =
suggestion || this.suggestions[this.highlighted]
const replacement = chosenSuggestion.replacement const replacement = chosenSuggestion.replacement
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement) const newValue = Completion.replaceWord(
this.modelValue,
this.wordAtCaret,
replacement
)
this.$emit('update:modelValue', newValue) this.$emit('update:modelValue', newValue)
this.highlighted = 0 this.highlighted = 0
const position = this.wordAtCaret.start + replacement.length const position = this.wordAtCaret.start + replacement.length
@ -295,7 +310,7 @@ const EmojiInput = {
e.preventDefault() e.preventDefault()
} }
}, },
cycleBackward (e) { cycleBackward(e) {
const len = this.suggestions.length || 0 const len = this.suggestions.length || 0
if (len > 1) { if (len > 1) {
this.highlighted -= 1 this.highlighted -= 1
@ -307,7 +322,7 @@ const EmojiInput = {
this.highlighted = 0 this.highlighted = 0
} }
}, },
cycleForward (e) { cycleForward(e) {
const len = this.suggestions.length || 0 const len = this.suggestions.length || 0
if (len > 1) { if (len > 1) {
this.highlighted += 1 this.highlighted += 1
@ -319,26 +334,28 @@ const EmojiInput = {
this.highlighted = 0 this.highlighted = 0
} }
}, },
scrollIntoView () { scrollIntoView() {
const rootRef = this.$refs['picker'].$el const rootRef = this.$refs['picker'].$el
/* Scroller is either `window` (replies in TL), sidebar (main post form, /* Scroller is either `window` (replies in TL), sidebar (main post form,
* replies in notifs) or mobile post form. Note that getting and setting * replies in notifs) or mobile post form. Note that getting and setting
* scroll is different for `Window` and `Element`s * scroll is different for `Window` and `Element`s
*/ */
const scrollerRef = this.$el.closest('.sidebar-scroller') || const scrollerRef =
this.$el.closest('.post-form-modal-view') || this.$el.closest('.sidebar-scroller') ||
window this.$el.closest('.post-form-modal-view') ||
const currentScroll = scrollerRef === window window
? scrollerRef.scrollY const currentScroll =
: scrollerRef.scrollTop scrollerRef === window ? scrollerRef.scrollY : scrollerRef.scrollTop
const scrollerHeight = scrollerRef === window const scrollerHeight =
? scrollerRef.innerHeight scrollerRef === window
: scrollerRef.offsetHeight ? scrollerRef.innerHeight
: scrollerRef.offsetHeight
const scrollerBottomBorder = currentScroll + scrollerHeight const scrollerBottomBorder = currentScroll + scrollerHeight
// We check where the bottom border of root element is, this uses findOffset // We check where the bottom border of root element is, this uses findOffset
// to find offset relative to scrollable container (scroller) // to find offset relative to scrollable container (scroller)
const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top const rootBottomBorder =
rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
const bottomDelta = Math.max(0, rootBottomBorder - scrollerBottomBorder) const bottomDelta = Math.max(0, rootBottomBorder - scrollerBottomBorder)
// could also check top delta but there's no case for it // could also check top delta but there's no case for it
@ -360,10 +377,10 @@ const EmojiInput = {
} }
}) })
}, },
onTransition (e) { onTransition(e) {
this.resize() this.resize()
}, },
onBlur (e) { onBlur(e) {
// Clicking on any suggestion removes focus from autocomplete, // Clicking on any suggestion removes focus from autocomplete,
// preventing click handler ever executing. // preventing click handler ever executing.
this.blurTimeout = setTimeout(() => { this.blurTimeout = setTimeout(() => {
@ -372,10 +389,10 @@ const EmojiInput = {
this.resize() this.resize()
}, 200) }, 200)
}, },
onClick (e, suggestion) { onClick(e, suggestion) {
this.replaceText(e, suggestion) this.replaceText(e, suggestion)
}, },
onFocus (e) { onFocus(e) {
if (this.blurTimeout) { if (this.blurTimeout) {
clearTimeout(this.blurTimeout) clearTimeout(this.blurTimeout)
this.blurTimeout = null this.blurTimeout = null
@ -389,7 +406,7 @@ const EmojiInput = {
this.resize() this.resize()
this.temporarilyHideSuggestions = false this.temporarilyHideSuggestions = false
}, },
onKeyUp (e) { onKeyUp(e) {
const { key } = e const { key } = e
this.setCaret(e) this.setCaret(e)
this.resize() this.resize()
@ -402,11 +419,11 @@ const EmojiInput = {
this.temporarilyHideSuggestions = false this.temporarilyHideSuggestions = false
} }
}, },
onPaste (e) { onPaste(e) {
this.setCaret(e) this.setCaret(e)
this.resize() this.resize()
}, },
onKeyDown (e) { onKeyDown(e) {
const { ctrlKey, shiftKey, key } = e const { ctrlKey, shiftKey, key } = e
if (this.newlineOnCtrlEnter && ctrlKey && key === 'Enter') { if (this.newlineOnCtrlEnter && ctrlKey && key === 'Enter') {
this.insert({ insertion: '\n', surroundingSpace: false }) this.insert({ insertion: '\n', surroundingSpace: false })
@ -453,31 +470,31 @@ const EmojiInput = {
this.showPicker = false this.showPicker = false
this.resize() this.resize()
}, },
onInput (e) { onInput(e) {
this.showPicker = false this.showPicker = false
this.setCaret(e) this.setCaret(e)
this.resize() this.resize()
this.$emit('update:modelValue', e.target.value) this.$emit('update:modelValue', e.target.value)
}, },
onClickInput (e) { onClickInput(e) {
this.showPicker = false this.showPicker = false
}, },
onClickOutside (e) { onClickOutside(e) {
if (this.disableClickOutside) return if (this.disableClickOutside) return
this.showPicker = false this.showPicker = false
}, },
onStickerUploaded (e) { onStickerUploaded(e) {
this.showPicker = false this.showPicker = false
this.$emit('sticker-uploaded', e) this.$emit('sticker-uploaded', e)
}, },
onStickerUploadFailed (e) { onStickerUploadFailed(e) {
this.showPicker = false this.showPicker = false
this.$emit('sticker-upload-Failed', e) this.$emit('sticker-upload-Failed', e)
}, },
setCaret ({ target: { selectionStart } }) { setCaret({ target: { selectionStart } }) {
this.caret = selectionStart this.caret = selectionStart
}, },
resize () { resize() {
const panel = this.$refs.panel const panel = this.$refs.panel
if (!panel) return if (!panel) return
const picker = this.$refs.picker.$el const picker = this.$refs.picker.$el
@ -488,9 +505,12 @@ const EmojiInput = {
this.setPlacement(panelBody, panel, offsetBottom) this.setPlacement(panelBody, panel, offsetBottom)
this.setPlacement(picker, picker, offsetBottom) this.setPlacement(picker, picker, offsetBottom)
}, },
setPlacement (container, target, offsetBottom) { setPlacement(container, target, offsetBottom) {
if (!container || !target) return if (!container || !target) return
if (this.placement === 'bottom' || (this.placement === 'auto' && !this.overflowsBottom(container))) { if (
this.placement === 'bottom' ||
(this.placement === 'auto' && !this.overflowsBottom(container))
) {
target.style.top = offsetBottom + 'px' target.style.top = offsetBottom + 'px'
target.style.bottom = 'auto' target.style.bottom = 'auto'
} else { } else {
@ -498,7 +518,7 @@ const EmojiInput = {
target.style.bottom = this.input.offsetHeight + 'px' target.style.bottom = this.input.offsetHeight + 'px'
} }
}, },
overflowsBottom (el) { overflowsBottom(el) {
return el.getBoundingClientRect().bottom > window.innerHeight return el.getBoundingClientRect().bottom > window.innerHeight
} }
} }

View file

@ -18,6 +18,7 @@
<EmojiPicker <EmojiPicker
v-if="enableEmojiPicker" v-if="enableEmojiPicker"
ref="picker" ref="picker"
show-keep-open
:class="{ hide: !showPicker }" :class="{ hide: !showPicker }"
:enable-sticker-picker="enableStickerPicker" :enable-sticker-picker="enableStickerPicker"
class="emoji-picker-panel" class="emoji-picker-panel"
@ -42,11 +43,14 @@
:class="{ highlighted: index === highlighted }" :class="{ highlighted: index === highlighted }"
@click.stop.prevent="onClick($event, suggestion)" @click.stop.prevent="onClick($event, suggestion)"
> >
<span v-if="!suggestion.mfm" class="image"> <span
v-if="!suggestion.mfm"
class="image"
>
<img <img
v-if="suggestion.img" v-if="suggestion.img"
:src="suggestion.img" :src="suggestion.img"
> />
<span v-else>{{ suggestion.replacement }}</span> <span v-else>{{ suggestion.replacement }}</span>
</span> </span>
<div class="label"> <div class="label">
@ -77,7 +81,7 @@
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
margin: .2em .25em; margin: 0.2em 0.25em;
font-size: 1.3em; font-size: 1.3em;
cursor: pointer; cursor: pointer;
line-height: 24px; line-height: 24px;
@ -93,7 +97,7 @@
margin-top: 2px; margin-top: 2px;
&.hide { &.hide {
display: none display: none;
} }
} }
@ -104,7 +108,7 @@
margin-top: 2px; margin-top: 2px;
&.hide { &.hide {
display: none display: none;
} }
&-body { &-body {
@ -178,7 +182,8 @@
} }
} }
input, textarea { input,
textarea {
flex: 1 0 auto; flex: 1 0 auto;
} }
} }

View file

@ -1,5 +1,26 @@
const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow', 'rotate', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4'] const MFM_TAGS = [
.map(tag => ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true })) 'blur',
'bounce',
'flip',
'font',
'jelly',
'jump',
'rainbow',
'rotate',
'shake',
'sparkle',
'spin',
'tada',
'twitch',
'x2',
'x3',
'x4'
].map((tag) => ({
displayText: tag,
detailText: '$[' + tag + ' ]',
replacement: '$[' + tag + ' ]',
mfm: true
}))
/** /**
* suggest - generates a suggestor function to be used by emoji-input * suggest - generates a suggestor function to be used by emoji-input
@ -13,10 +34,10 @@ const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow',
* doesn't support user linking you can just provide only emoji. * doesn't support user linking you can just provide only emoji.
*/ */
export default data => { export default (data) => {
const emojiCurry = suggestEmoji(data.emoji) const emojiCurry = suggestEmoji(data.emoji)
const usersCurry = data.store && suggestUsers(data.store) const usersCurry = data.store && suggestUsers(data.store)
return input => { return (input) => {
const firstChar = input[0] const firstChar = input[0]
if (firstChar === ':' && data.emoji) { if (firstChar === ':' && data.emoji) {
return emojiCurry(input) return emojiCurry(input)
@ -25,14 +46,15 @@ export default data => {
return usersCurry(input) return usersCurry(input)
} }
if (firstChar === '$') { if (firstChar === '$') {
return MFM_TAGS return MFM_TAGS.filter(
.filter(({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1) ({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1
)
} }
return [] return []
} }
} }
export const suggestEmoji = emojis => input => { export const suggestEmoji = (emojis) => (input) => {
const noPrefix = input.toLowerCase().substr(1) const noPrefix = input.toLowerCase().substr(1)
return emojis return emojis
.filter(({ displayText }) => displayText.toLowerCase().match(noPrefix)) .filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
@ -85,7 +107,7 @@ export const suggestUsers = ({ dispatch, state }) => {
}) })
} }
return async input => { return async (input) => {
const noPrefix = input.toLowerCase().substr(1) const noPrefix = input.toLowerCase().substr(1)
if (previousQuery === noPrefix) return suggestions if (previousQuery === noPrefix) return suggestions
@ -99,36 +121,47 @@ export const suggestUsers = ({ dispatch, state }) => {
await debounceUserSearch(noPrefix) await debounceUserSearch(noPrefix)
} }
const newSuggestions = state.users.users.filter( const newSuggestions = state.users.users
user => .filter(
user.screen_name.toLowerCase().startsWith(noPrefix) || (user) =>
user.name.toLowerCase().startsWith(noPrefix) user.screen_name.toLowerCase().startsWith(noPrefix) ||
).slice(0, 20).sort((a, b) => { user.name.toLowerCase().startsWith(noPrefix)
let aScore = 0 )
let bScore = 0 .slice(0, 20)
.sort((a, b) => {
let aScore = 0
let bScore = 0
// Matches on screen name (i.e. user@instance) makes a priority // Matches on screen name (i.e. user@instance) makes a priority
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0 aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0 bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
// Matches on name takes second priority // Matches on name takes second priority
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0 aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0 bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
const diff = (bScore - aScore) * 10 const diff = (bScore - aScore) * 10
// Then sort alphabetically // Then sort alphabetically
const nameAlphabetically = a.name > b.name ? 1 : -1 const nameAlphabetically = a.name > b.name ? 1 : -1
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1 const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
return diff + nameAlphabetically + screenNameAlphabetically return diff + nameAlphabetically + screenNameAlphabetically
/* eslint-disable camelcase */ /* eslint-disable camelcase */
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({ })
displayText: screen_name_ui, .map(
detailText: name, ({
imageUrl: profile_image_url_original, screen_name,
replacement: '@' + screen_name + ' ' screen_name_ui,
})) name,
profile_image_url_original
}) => ({
displayText: screen_name_ui,
detailText: name,
imageUrl: profile_image_url_original,
replacement: '@' + screen_name + ' '
})
)
/* eslint-enable camelcase */ /* eslint-enable camelcase */
suggestions = newSuggestions || [] suggestions = newSuggestions || []

View file

@ -1,5 +1,6 @@
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import EmojiGrid from '../emoji_grid/emoji_grid.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faBoxOpen, faBoxOpen,
@ -8,18 +9,7 @@ import {
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { trim, escapeRegExp, startCase } from 'lodash' import { trim, escapeRegExp, startCase } from 'lodash'
library.add( library.add(faBoxOpen, faStickyNote, faSmileBeam)
faBoxOpen,
faStickyNote,
faSmileBeam
)
// At widest, approximately 20 emoji are visible in a row,
// loading 3 rows, could be overkill for narrow picker
const LOAD_EMOJI_BY = 60
// When to start loading new batch emoji, in pixels
const LOAD_EMOJI_MARGIN = 64
const EmojiPicker = { const EmojiPicker = {
props: { props: {
@ -27,151 +17,88 @@ const EmojiPicker = {
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false
},
showKeepOpen: {
required: false,
type: Boolean,
default: false
} }
}, },
data () { data() {
return { return {
keyword: '', keyword: '',
activeGroup: 'standard', activeGroup: 'standard',
showingStickers: false, showingStickers: false,
groupsScrolledClass: 'scrolled-top', keepOpen: false
keepOpen: false,
customEmojiBufferSlice: LOAD_EMOJI_BY,
customEmojiTimeout: null,
customEmojiLoadAllConfirmed: false
} }
}, },
components: { components: {
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')), StickerPicker: defineAsyncComponent(() =>
Checkbox import('../sticker_picker/sticker_picker.vue')
),
Checkbox,
EmojiGrid
}, },
methods: { methods: {
onStickerUploaded (e) { onStickerUploaded(e) {
this.$emit('sticker-uploaded', e) this.$emit('sticker-uploaded', e)
}, },
onStickerUploadFailed (e) { onStickerUploadFailed(e) {
this.$emit('sticker-upload-failed', e) this.$emit('sticker-upload-failed', e)
}, },
onEmoji (emoji) { onEmoji(emoji) {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement const value = emoji.imageUrl
? `:${emoji.displayText}:`
: emoji.replacement
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen }) this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
this.$store.commit('emojiUsed', emoji)
}, },
onScroll (e) { onWheel(e) {
const target = (e && e.target) || this.$refs['emoji-groups']
this.updateScrolledClass(target)
this.scrolledGroup(target)
this.triggerLoadMore(target)
},
onWheel (e) {
e.preventDefault() e.preventDefault()
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0) this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
}, },
highlight (key) { highlight(key) {
this.setShowStickers(false) this.setShowStickers(false)
this.activeGroup = key this.activeGroup = key
}, if (this.keyword.length) {
updateScrolledClass (target) { this.$refs.emojiGrid.scrollToItem(key)
if (target.scrollTop <= 5) {
this.groupsScrolledClass = 'scrolled-top'
} else if (target.scrollTop >= target.scrollTopMax - 5) {
this.groupsScrolledClass = 'scrolled-bottom'
} else {
this.groupsScrolledClass = 'scrolled-middle'
} }
}, },
triggerLoadMore (target) { onActiveGroup(group) {
const ref = this.$refs['group-end-custom'] this.activeGroup = group
if (!ref) return
const bottom = ref.offsetTop + ref.offsetHeight
const scrollerBottom = target.scrollTop + target.clientHeight
const scrollerTop = target.scrollTop
const scrollerMax = target.scrollHeight
// Loads more emoji when they come into view
const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
// Always load when at the very top in case there's no scroll space yet
const atTop = scrollerTop < 5
// Don't load when looking at unicode category or at the very bottom
const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
if (!bottomAboveViewport && (approachingBottom || atTop)) {
this.loadEmoji()
}
}, },
scrolledGroup (target) { toggleStickers() {
const top = target.scrollTop + 5
this.$nextTick(() => {
this.emojisView.forEach(group => {
const ref = this.$refs['group-' + group.id]
if (ref.offsetTop <= top) {
this.activeGroup = group.id
}
})
})
},
loadEmoji () {
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
if (allLoaded) {
return
}
this.customEmojiBufferSlice += LOAD_EMOJI_BY
},
startEmojiLoad (forceUpdate = false) {
if (!forceUpdate) {
this.keyword = ''
}
this.$nextTick(() => {
this.$refs['emoji-groups'].scrollTop = 0
})
const bufferSize = this.customEmojiBuffer.length
const bufferPrefilledAll = bufferSize === this.filteredEmoji.length
if (bufferPrefilledAll && !forceUpdate) {
return
}
this.customEmojiBufferSlice = LOAD_EMOJI_BY
},
toggleStickers () {
this.showingStickers = !this.showingStickers this.showingStickers = !this.showingStickers
}, },
setShowStickers (value) { setShowStickers(value) {
this.showingStickers = value this.showingStickers = value
}, },
filterByKeyword (list) { filterByKeyword(list) {
if (this.keyword === '') return list if (this.keyword === '') return list
const regex = new RegExp(escapeRegExp(trim(this.keyword)), 'i') const regex = new RegExp(escapeRegExp(trim(this.keyword)), 'i')
return list.filter(emoji => { return list.filter((emoji) => {
return (regex.test(emoji.displayText) || (!emoji.imageUrl && emoji.replacement === this.keyword)) return (
regex.test(emoji.displayText) ||
(!emoji.imageUrl && emoji.replacement === this.keyword)
)
}) })
} }
}, },
watch: {
keyword () {
this.customEmojiLoadAllConfirmed = false
this.onScroll()
this.startEmojiLoad(true)
}
},
computed: { computed: {
activeGroupView () { activeGroupView() {
return this.showingStickers ? '' : this.activeGroup return this.showingStickers ? '' : this.activeGroup
}, },
stickersAvailable () { stickersAvailable() {
if (this.$store.state.instance.stickers) { if (this.$store.state.instance.stickers) {
return this.$store.state.instance.stickers.length > 0 return this.$store.state.instance.stickers.length > 0
} }
return 0 return 0
}, },
filteredEmoji () { filteredEmoji() {
return this.filterByKeyword( return this.filterByKeyword(this.$store.state.instance.customEmoji || [])
this.$store.state.instance.customEmoji || []
)
}, },
customEmojiBuffer () { emojis() {
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice) const recentEmojis = this.$store.getters.recentEmojis
},
emojis () {
const standardEmojis = this.$store.state.instance.emoji || [] const standardEmojis = this.$store.state.instance.emoji || []
const customEmojis = this.sortedEmoji const customEmojis = this.sortedEmoji
const emojiPacks = [] const emojiPacks = []
@ -184,6 +111,15 @@ const EmojiPicker = {
}) })
}) })
return [ return [
{
id: 'recent',
text: this.$t('emoji.recent'),
first: {
imageUrl: '',
replacement: '🕒'
},
emojis: this.filterByKeyword(recentEmojis)
},
{ {
id: 'standard', id: 'standard',
text: this.$t('emoji.unicode'), text: this.$t('emoji.unicode'),
@ -195,7 +131,7 @@ const EmojiPicker = {
} }
].concat(emojiPacks) ].concat(emojiPacks)
}, },
sortedEmoji () { sortedEmoji() {
const customEmojis = this.$store.state.instance.customEmoji || [] const customEmojis = this.$store.state.instance.customEmoji || []
const sortedEmojiGroups = new Map() const sortedEmojiGroups = new Map()
customEmojis.forEach((emoji) => { customEmojis.forEach((emoji) => {
@ -207,19 +143,22 @@ const EmojiPicker = {
}) })
return new Map([...sortedEmojiGroups.entries()].sort()) return new Map([...sortedEmojiGroups.entries()].sort())
}, },
emojisView () { emojisView() {
if (this.keyword === '') { if (this.keyword === '') {
return this.emojis.filter(pack => { return this.emojis.filter((pack) => {
return pack.id === this.activeGroup return pack.id === this.activeGroup
}) })
} else { } else {
return this.emojis.filter(pack => { return this.emojis.filter((pack) => {
return pack.emojis.length > 0 return pack.emojis.length > 0
}) })
} }
}, },
stickerPickerEnabled () { stickerPickerEnabled() {
return (this.$store.state.instance.stickers || []).length !== 0 && this.enableStickerPicker return (
(this.$store.state.instance.stickers || []).length !== 0 &&
this.enableStickerPicker
)
} }
} }
} }

View file

@ -1,5 +1,23 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
// The worst query selector ever
// selects ONLY emojis pickers in replies in notifications
// who thought this was a good idea?
.notification
> .Status
> .status-container
> .post-status-form
> form
> .form-group
> .emoji-input
> .emoji-picker {
max-width: 100%;
left: 0;
@media (min-width: 1300px) {
left: -30px;
}
}
.Notification { .Notification {
.emoji-picker { .emoji-picker {
min-width: 160%; min-width: 160%;
@ -7,7 +25,7 @@
overflow: hidden; overflow: hidden;
left: -70%; left: -70%;
max-width: 100%; max-width: 100%;
@media (min-width: 800px) and (max-width: 1300px) { @media (min-width: 800px) and (max-width: 1280px) {
left: -50%; left: -50%;
min-width: 50%; min-width: 50%;
max-width: 130%; max-width: 130%;
@ -18,6 +36,10 @@
min-width: 50%; min-width: 50%;
max-width: 130%; max-width: 130%;
} }
.Status > .emoji-picker {
z-index: 1000;
}
} }
} }
.emoji-picker { .emoji-picker {
@ -70,10 +92,6 @@
flex-grow: 1; flex-grow: 1;
} }
.emoji-groups {
min-height: 200px;
}
.additional-tabs { .additional-tabs {
border-left: 1px solid; border-left: 1px solid;
border-left-color: $fallback--icon; border-left-color: $fallback--icon;
@ -100,7 +118,7 @@
justify-content: center; justify-content: center;
width: 32px; width: 32px;
height: 32px; height: 32px;
padding: .4em; padding: 0.4em;
cursor: pointer; cursor: pointer;
img { img {
@ -133,7 +151,7 @@
} }
.sticker-picker { .sticker-picker {
flex: 1 1 auto flex: 1 1 auto;
} }
.stickers, .stickers,
@ -152,14 +170,12 @@
} }
} }
.emoji { .emoji-search {
&-search { padding: 5px;
padding: 5px; flex: 0 0 auto;
flex: 0 0 auto;
input { input {
width: 100%; width: 100%;
}
} }
&-groups { &-groups {
@ -168,8 +184,8 @@
overflow: auto; overflow: auto;
user-select: none; user-select: none;
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat, mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat, linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
linear-gradient(to top, white, white); linear-gradient(to top, white, white);
transition: mask-size 150ms; transition: mask-size 150ms;
mask-size: 100% 20px, 100% 20px, auto; mask-size: 100% 20px, 100% 20px, auto;
// Autoprefixed seem to ignore this one, and also syntax is different // Autoprefixed seem to ignore this one, and also syntax is different
@ -221,7 +237,5 @@
max-height: 100%; max-height: 100%;
} }
} }
} }
} }

View file

@ -2,9 +2,9 @@
<div class="emoji-picker panel panel-default panel-body"> <div class="emoji-picker panel panel-default panel-body">
<div class="heading"> <div class="heading">
<span <span
ref="emoji-tabs"
class="emoji-tabs" class="emoji-tabs"
@wheel="onWheel" @wheel="onWheel"
ref="emoji-tabs"
> >
<span <span
v-for="group in emojis" v-for="group in emojis"
@ -17,16 +17,18 @@
:title="group.text" :title="group.text"
@click.prevent="highlight(group.id)" @click.prevent="highlight(group.id)"
> >
<span v-if="!group.first.imageUrl">{{ group.first.replacement }}</span> <span v-if="!group.first.imageUrl">{{
group.first.replacement
}}</span>
<img <img
v-else v-else
:src="group.first.imageUrl" :src="group.first.imageUrl"
> />
</span> </span>
<span <span
v-if="stickerPickerEnabled" v-if="stickerPickerEnabled"
class="stickers-tab-icon emoji-tabs-item" class="stickers-tab-icon emoji-tabs-item"
:class="{active: showingStickers}" :class="{ active: showingStickers }"
:title="$t('emoji.stickers')" :title="$t('emoji.stickers')"
@click.prevent="toggleStickers" @click.prevent="toggleStickers"
> >
@ -40,7 +42,7 @@
<div class="content"> <div class="content">
<div <div
class="emoji-content" class="emoji-content"
:class="{hidden: showingStickers}" :class="{ hidden: showingStickers }"
> >
<div class="emoji-search"> <div class="emoji-search">
<input <input
@ -49,13 +51,17 @@
class="form-control" class="form-control"
:placeholder="$t('emoji.search_emoji')" :placeholder="$t('emoji.search_emoji')"
@input="$event.target.composing = false" @input="$event.target.composing = false"
> />
</div> </div>
<EmojiGrid
ref="emojiGrid"
:groups="emojisView"
@emoji="onEmoji"
@active-group="onActiveGroup"
/>
<div <div
ref="emoji-groups" v-if="showKeepOpen"
class="emoji-groups" class="keep-open"
:class="groupsScrolledClass"
@scroll="onScroll"
> >
<div <div
v-for="group in emojisView" v-for="group in emojisView"
@ -79,7 +85,7 @@
<img <img
v-else v-else
:src="emoji.imageUrl" :src="emoji.imageUrl"
> />
</span> </span>
<span :ref="'group-end-' + group.id" /> <span :ref="'group-end-' + group.id" />
</div> </div>

View file

@ -3,6 +3,11 @@ import UserListPopover from '../user_list_popover/user_list_popover.vue'
const EMOJI_REACTION_COUNT_CUTOFF = 12 const EMOJI_REACTION_COUNT_CUTOFF = 12
const findEmojiByReplacement = (state, replacement) => {
const allEmojis = state.instance.emoji.concat(state.instance.customEmoji)
return allEmojis.find((emoji) => emoji.replacement === replacement)
}
const EmojiReactions = { const EmojiReactions = {
name: 'EmojiReactions', name: 'EmojiReactions',
components: { components: {
@ -14,18 +19,20 @@ const EmojiReactions = {
showAll: false showAll: false
}), }),
computed: { computed: {
tooManyReactions () { tooManyReactions() {
return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF
}, },
emojiReactions () { emojiReactions() {
return this.showAll return this.showAll
? this.status.emoji_reactions ? this.status.emoji_reactions
: this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF) : this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)
}, },
showMoreString () { showMoreString() {
return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}` return `+${
this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF
}`
}, },
accountsForEmoji () { accountsForEmoji() {
return this.status.emoji_reactions.reduce((acc, reaction) => { return this.status.emoji_reactions.reduce((acc, reaction) => {
if (reaction.url) { if (reaction.url) {
acc[reaction.url] = reaction.accounts || [] acc[reaction.url] = reaction.accounts || []
@ -35,30 +42,32 @@ const EmojiReactions = {
return acc return acc
}, {}) }, {})
}, },
loggedIn () { loggedIn() {
return !!this.$store.state.users.currentUser return !!this.$store.state.users.currentUser
} }
}, },
methods: { methods: {
toggleShowAll () { toggleShowAll() {
this.showAll = !this.showAll this.showAll = !this.showAll
}, },
reactedWith (emoji) { reactedWith(emoji) {
return this.status.emoji_reactions.find(r => r.name === emoji).me return this.status.emoji_reactions.find((r) => r.name === emoji).me
}, },
fetchEmojiReactionsByIfMissing () { fetchEmojiReactionsByIfMissing() {
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts) const hasNoAccounts = this.status.emoji_reactions.find((r) => !r.accounts)
if (hasNoAccounts) { if (hasNoAccounts) {
this.$store.dispatch('fetchEmojiReactionsBy', this.status.id) this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
} }
}, },
reactWith (emoji) { reactWith(emoji) {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
const emojiObject = findEmojiByReplacement(this.$store.state, emoji)
this.$store.commit('emojiUsed', emojiObject)
}, },
unreact (emoji) { unreact(emoji) {
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji }) this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
}, },
emojiOnClick (emoji, event) { emojiOnClick(emoji, event) {
if (!this.loggedIn) return if (!this.loggedIn) return
if (this.reactedWith(emoji)) { if (this.reactedWith(emoji)) {

View file

@ -1,28 +1,35 @@
<template> <template>
<div class="emoji-reactions"> <div class="emoji-reactions">
<UserListPopover <UserListPopover
v-for="(reaction) in emojiReactions" v-for="reaction in emojiReactions"
:key="reaction.url || reaction.name" :key="reaction.url || reaction.name"
:users="accountsForEmoji[reaction.url || reaction.name]" :users="accountsForEmoji[reaction.url || reaction.name]"
> >
<button <button
class="emoji-reaction btn button-default" class="emoji-reaction btn button-default"
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }" :class="{
'picked-reaction': reactedWith(reaction.name),
'not-clickable': !loggedIn
}"
@click="emojiOnClick(reaction.name, $event)" @click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()" @mouseenter="fetchEmojiReactionsByIfMissing()"
> >
<span <span
v-if="reaction.url !== null" v-if="reaction.url !== null"
class="emoji-button-inner"
> >
<img <img
:src="reaction.url" :src="reaction.url"
:title="reaction.name" :title="reaction.name"
class="reaction-emoji" class="reaction-emoji"
width="2.55em" width="2.55em"
> />
{{ reaction.count }} {{ reaction.count }}
</span> </span>
<span v-else> <span
v-else
class="emoji-button-inner"
>
<span class="reaction-emoji unicode-emoji"> <span class="reaction-emoji unicode-emoji">
{{ reaction.name }} {{ reaction.name }}
</span> </span>
@ -41,7 +48,7 @@
</div> </div>
</template> </template>
<script src="./emoji_reactions.js" ></script> <script src="./emoji_reactions.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
@ -49,10 +56,11 @@
display: flex; display: flex;
margin-top: 0.25em; margin-top: 0.25em;
flex-wrap: wrap; flex-wrap: wrap;
container-type: inline-size;
} }
.unicode-emoji { .unicode-emoji {
font-size: 210%; font-size: 128%;
} }
.emoji-reaction { .emoji-reaction {
@ -60,13 +68,20 @@
margin-right: 0.5em; margin-right: 0.5em;
margin-top: 0.5em; margin-top: 0.5em;
display: flex; display: flex;
height: 28px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-sizing: border-box; box-sizing: border-box;
.reaction-emoji { .reaction-emoji {
width: 2.55em !important; width: auto;
max-width: 96cqw;
height: 2.55em !important;
margin-right: 0.25em; margin-right: 0.25em;
} }
img.reaction-emoji {
width: 1.55em !important;
display: block;
}
&:focus { &:focus {
outline: none; outline: none;
} }
@ -93,9 +108,12 @@
} }
.button-default.picked-reaction { .button-default.picked-reaction {
border: 1px solid var(--accent, $fallback--link); background: none;
margin-left: -1px; // offset the border, can't use inset shadows either padding: 1px 0.5em;
margin-right: calc(0.5em - 1px);
}
.emoji-button-inner {
display: flex;
align-items: center;
}
}
</style> </style>

View file

@ -1,9 +1,7 @@
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faCircleNotch)
faCircleNotch
)
const Exporter = { const Exporter = {
props: { props: {
@ -18,26 +16,30 @@ const Exporter = {
exportButtonLabel: { type: String }, exportButtonLabel: { type: String },
processingMessage: { type: String } processingMessage: { type: String }
}, },
data () { data() {
return { return {
processing: false processing: false
} }
}, },
methods: { methods: {
process () { process() {
this.processing = true this.processing = true
this.getContent() this.getContent().then((content) => {
.then((content) => { const fileToDownload = document.createElement('a')
const fileToDownload = document.createElement('a') fileToDownload.setAttribute(
fileToDownload.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content)) 'href',
fileToDownload.setAttribute('download', this.filename) 'data:text/plain;charset=utf-8,' + encodeURIComponent(content)
fileToDownload.style.display = 'none' )
document.body.appendChild(fileToDownload) fileToDownload.setAttribute('download', this.filename)
fileToDownload.click() fileToDownload.style.display = 'none'
document.body.removeChild(fileToDownload) document.body.appendChild(fileToDownload)
// Add delay before hiding processing state since browser takes some time to handle file download fileToDownload.click()
setTimeout(() => { this.processing = false }, 2000) document.body.removeChild(fileToDownload)
}) // Add delay before hiding processing state since browser takes some time to handle file download
setTimeout(() => {
this.processing = false
}, 2000)
})
} }
} }
} }

View file

@ -15,6 +15,7 @@ import {
faBookmark as faBookmarkReg, faBookmark as faBookmarkReg,
faFlag faFlag
} from '@fortawesome/free-regular-svg-icons' } from '@fortawesome/free-regular-svg-icons'
import { mapState } from 'vuex'
library.add( library.add(
faEllipsisH, faEllipsisH,
@ -35,7 +36,7 @@ const ExtraButtons = {
Popover, Popover,
ConfirmModal ConfirmModal
}, },
data () { data() {
return { return {
expanded: false, expanded: false,
showingDeleteDialog: false, showingDeleteDialog: false,
@ -43,154 +44,206 @@ const ExtraButtons = {
} }
}, },
methods: { methods: {
deleteStatus () { deleteStatus() {
if (this.shouldConfirmDelete) { if (this.shouldConfirmDelete) {
this.showDeleteStatusConfirmDialog() this.showDeleteStatusConfirmDialog()
} else { } else {
this.doDeleteStatus() this.doDeleteStatus()
} }
}, },
doDeleteStatus () { doDeleteStatus() {
this.$store.dispatch('deleteStatus', { id: this.status.id }) this.$store.dispatch('deleteStatus', { id: this.status.id })
this.hideDeleteStatusConfirmDialog() this.hideDeleteStatusConfirmDialog()
}, },
showDeleteStatusConfirmDialog () { showDeleteStatusConfirmDialog() {
this.showingDeleteDialog = true this.showingDeleteDialog = true
}, },
hideDeleteStatusConfirmDialog () { hideDeleteStatusConfirmDialog() {
this.showingDeleteDialog = false this.showingDeleteDialog = false
}, },
translateStatus () { translateStatus() {
if (this.noTranslationTargetSet) { if (this.noTranslationTargetSet) {
this.$store.dispatch('pushGlobalNotice', { messageKey: 'toast.no_translation_target_set', level: 'info' }) this.$store.dispatch('pushGlobalNotice', {
messageKey: 'toast.no_translation_target_set',
level: 'info'
})
} }
const translateTo = this.$store.getters.mergedConfig.translationLanguage || this.$store.state.instance.interfaceLanguage const translateTo =
this.$store.dispatch('translateStatus', { id: this.status.id, language: translateTo }) this.$store.getters.mergedConfig.translationLanguage ||
this.$store.state.instance.interfaceLanguage
this.$store
.dispatch('translateStatus', {
id: this.status.id,
language: translateTo
})
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err.error.error))
}, },
pinStatus () { pinStatus() {
this.$store.dispatch('pinStatus', this.status.id) this.$store
.dispatch('pinStatus', this.status.id)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err.error.error))
}, },
unpinStatus () { unpinStatus() {
this.$store.dispatch('unpinStatus', this.status.id) this.$store
.dispatch('unpinStatus', this.status.id)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err.error.error))
}, },
muteConversation () { muteConversation() {
this.$store.dispatch('muteConversation', this.status.id) this.$store
.dispatch('muteConversation', this.status.id)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err.error.error))
}, },
unmuteConversation () { unmuteConversation() {
this.$store.dispatch('unmuteConversation', this.status.id) this.$store
.dispatch('unmuteConversation', this.status.id)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err.error.error))
}, },
copyLink () { copyLink() {
navigator.clipboard.writeText(this.statusLink) navigator.clipboard
.writeText(this.statusLink)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err.error.error))
}, },
bookmarkStatus () { bookmarkStatus() {
this.$store.dispatch('bookmark', { id: this.status.id }) this.$store
.dispatch('bookmark', { id: this.status.id })
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err.error.error))
}, },
unbookmarkStatus () { unbookmarkStatus() {
this.$store.dispatch('unbookmark', { id: this.status.id }) this.$store
.dispatch('unbookmark', { id: this.status.id })
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch((err) => this.$emit('onError', err.error.error))
}, },
reportStatus () { reportStatus() {
this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] }) this.$store.dispatch('openUserReportingModal', {
userId: this.status.user.id,
statusIds: [this.status.id]
})
}, },
editStatus () { editStatus() {
this.$store.dispatch('fetchStatusSource', { id: this.status.id }) this.$store
.then(data => this.$store.dispatch('openEditStatusModal', { .dispatch('fetchStatusSource', { id: this.status.id })
statusId: this.status.id, .then((data) =>
subject: data.spoiler_text, this.$store.dispatch('openEditStatusModal', {
statusText: data.text, statusId: this.status.id,
statusIsSensitive: this.status.nsfw, subject: data.spoiler_text,
statusPoll: this.status.poll, statusText: data.text,
statusFiles: [...this.status.attachments], statusIsSensitive: this.status.nsfw,
visibility: this.status.visibility, statusPoll: this.status.poll,
statusContentType: data.content_type statusFiles: [...this.status.attachments],
})) visibility: this.status.visibility,
statusContentType: data.content_type
})
)
}, },
showStatusHistory () { showStatusHistory() {
const originalStatus = { ...this.status } const originalStatus = { ...this.status }
const stripFieldsList = ['attachments', 'created_at', 'emojis', 'text', 'raw_html', 'nsfw', 'poll', 'summary', 'summary_raw_html'] const stripFieldsList = [
stripFieldsList.forEach(p => delete originalStatus[p]) 'attachments',
'created_at',
'emojis',
'text',
'raw_html',
'nsfw',
'poll',
'summary',
'summary_raw_html'
]
stripFieldsList.forEach((p) => delete originalStatus[p])
this.$store.dispatch('openStatusHistoryModal', originalStatus) this.$store.dispatch('openStatusHistoryModal', originalStatus)
}, },
redraftStatus () { redraftStatus() {
if (this.shouldConfirmDelete) { if (this.shouldConfirmDelete) {
this.showRedraftStatusConfirmDialog() this.showRedraftStatusConfirmDialog()
} else { } else {
this.doRedraftStatus() this.doRedraftStatus()
} }
}, },
doRedraftStatus () { doRedraftStatus() {
this.$store.dispatch('fetchStatusSource', { id: this.status.id }) this.$store
.then(data => this.$store.dispatch('openPostStatusModal', { .dispatch('fetchStatusSource', { id: this.status.id })
isRedraft: true, .then((data) =>
statusId: this.status.id, this.$store.dispatch('openPostStatusModal', {
subject: data.spoiler_text, isRedraft: true,
statusText: data.text, statusId: this.status.id,
statusIsSensitive: this.status.nsfw, subject: data.spoiler_text,
statusPoll: this.status.poll, statusText: data.text,
statusFiles: [...this.status.attachments], statusIsSensitive: this.status.nsfw,
statusScope: this.status.visibility, statusPoll: this.status.poll,
statusContentType: data.content_type statusFiles: [...this.status.attachments],
})) statusScope: this.status.visibility,
statusLanguage: this.status.language,
statusContentType: data.content_type
})
)
this.doDeleteStatus() this.doDeleteStatus()
}, },
showRedraftStatusConfirmDialog () { showRedraftStatusConfirmDialog() {
this.showingRedraftDialog = true this.showingRedraftDialog = true
}, },
hideRedraftStatusConfirmDialog () { hideRedraftStatusConfirmDialog() {
this.showingRedraftDialog = false this.showingRedraftDialog = false
} }
}, },
computed: { computed: {
currentUser () { return this.$store.state.users.currentUser }, currentUser() {
canDelete () { return this.$store.state.users.currentUser
if (!this.currentUser) { return } },
const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin canDelete() {
if (!this.currentUser) {
return
}
const superuser =
this.currentUser.rights.moderator || this.currentUser.rights.admin
return superuser || this.status.user.id === this.currentUser.id return superuser || this.status.user.id === this.currentUser.id
}, },
ownStatus () { ownStatus() {
return this.status.user.id === this.currentUser.id return this.status.user.id === this.currentUser.id
}, },
canPin () { canPin() {
return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted') return (
this.ownStatus &&
(this.status.visibility === 'public' ||
this.status.visibility === 'unlisted')
)
}, },
canMute () { canMute() {
return !!this.currentUser return !!this.currentUser
}, },
canTranslate () { canTranslate() {
return this.$store.state.instance.translationEnabled === true return this.$store.state.instance.translationEnabled === true
}, },
noTranslationTargetSet () { noTranslationTargetSet() {
return this.$store.getters.mergedConfig.translationLanguage === undefined return this.$store.getters.mergedConfig.translationLanguage === undefined
}, },
statusLink () { statusLink() {
if (this.status.is_local) { if (this.status.is_local) {
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}` return `${this.$store.state.instance.server}${
this.$router.resolve({
name: 'conversation',
params: { id: this.status.id }
}).href
}`
} else { } else {
return this.status.external_url return this.status.external_url
} }
}, },
shouldConfirmDelete () { shouldConfirmDelete() {
return this.$store.getters.mergedConfig.modalOnDelete return this.$store.getters.mergedConfig.modalOnDelete
}, },
isEdited () { isEdited() {
return this.status.edited_at !== null return this.status.edited_at !== null
}, },
editingAvailable () { return this.$store.state.instance.editingAvailable } editingAvailable() {
return this.$store.state.instance.editingAvailable
}
} }
} }

View file

@ -7,7 +7,7 @@
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
remove-padding remove-padding
> >
<template v-slot:content="{close}"> <template #content="{ close }">
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <button
v-if="canMute && !status.thread_muted" v-if="canMute && !status.thread_muted"
@ -17,7 +17,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="eye-slash" icon="eye-slash"
/><span>{{ $t("status.mute_conversation") }}</span> /><span>{{ $t('status.mute_conversation') }}</span>
</button> </button>
<button <button
v-if="canMute && status.thread_muted" v-if="canMute && status.thread_muted"
@ -27,7 +27,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="eye-slash" icon="eye-slash"
/><span>{{ $t("status.unmute_conversation") }}</span> /><span>{{ $t('status.unmute_conversation') }}</span>
</button> </button>
<button <button
v-if="!status.pinned && canPin" v-if="!status.pinned && canPin"
@ -38,7 +38,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="thumbtack" icon="thumbtack"
/><span>{{ $t("status.pin") }}</span> /><span>{{ $t('status.pin') }}</span>
</button> </button>
<button <button
v-if="status.pinned && canPin" v-if="status.pinned && canPin"
@ -49,7 +49,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="thumbtack" icon="thumbtack"
/><span>{{ $t("status.unpin") }}</span> /><span>{{ $t('status.unpin') }}</span>
</button> </button>
<button <button
v-if="!status.bookmarked" v-if="!status.bookmarked"
@ -60,7 +60,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
:icon="['far', 'bookmark']" :icon="['far', 'bookmark']"
/><span>{{ $t("status.bookmark") }}</span> /><span>{{ $t('status.bookmark') }}</span>
</button> </button>
<button <button
v-if="status.bookmarked" v-if="status.bookmarked"
@ -71,7 +71,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="bookmark" icon="bookmark"
/><span>{{ $t("status.unbookmark") }}</span> /><span>{{ $t('status.unbookmark') }}</span>
</button> </button>
<button <button
v-if="ownStatus && editingAvailable" v-if="ownStatus && editingAvailable"
@ -82,7 +82,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="pen" icon="pen"
/><span>{{ $t("status.edit") }}</span> /><span>{{ $t('status.edit') }}</span>
</button> </button>
<button <button
v-if="isEdited && editingAvailable" v-if="isEdited && editingAvailable"
@ -93,7 +93,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="history" icon="history"
/><span>{{ $t("status.edit_history") }}</span> /><span>{{ $t('status.edit_history') }}</span>
</button> </button>
<button <button
v-if="ownStatus" v-if="ownStatus"
@ -104,7 +104,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="file-pen" icon="file-pen"
/><span>{{ $t("status.redraft") }}</span> /><span>{{ $t('status.redraft') }}</span>
</button> </button>
<button <button
v-if="canDelete" v-if="canDelete"
@ -115,7 +115,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="times" icon="times"
/><span>{{ $t("status.delete") }}</span> /><span>{{ $t('status.delete') }}</span>
</button> </button>
<button <button
class="button-default dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@ -125,7 +125,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="share-alt" icon="share-alt"
/><span>{{ $t("status.copy_link") }}</span> /><span>{{ $t('status.copy_link') }}</span>
</button> </button>
<a <a
v-if="!status.is_local" v-if="!status.is_local"
@ -137,7 +137,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="external-link-alt" icon="external-link-alt"
/><span>{{ $t("status.external_source") }}</span> /><span>{{ $t('status.external_source') }}</span>
</a> </a>
<button <button
class="button-default dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@ -147,7 +147,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
:icon="['far', 'flag']" :icon="['far', 'flag']"
/><span>{{ $t("user_card.report") }}</span> /><span>{{ $t('user_card.report') }}</span>
</button> </button>
<button <button
v-if="canTranslate" v-if="canTranslate"
@ -158,7 +158,7 @@
<FAIcon <FAIcon
fixed-width fixed-width
icon="globe" icon="globe"
/><span>{{ $t("status.translate") }}</span> /><span>{{ $t('status.translate') }}</span>
<template v-if="noTranslationTargetSet"> <template v-if="noTranslationTargetSet">
<span class="dropdown-item-icon__badge warning"> <span class="dropdown-item-icon__badge warning">
@ -172,7 +172,7 @@
</button> </button>
</div> </div>
</template> </template>
<template v-slot:trigger> <template #trigger>
<button class="button-unstyled popover-trigger"> <button class="button-unstyled popover-trigger">
<FAIcon <FAIcon
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
@ -205,7 +205,7 @@
</Popover> </Popover>
</template> </template>
<script src="./extra_buttons.js" ></script> <script src="./extra_buttons.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -1,24 +1,19 @@
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faStar } from '@fortawesome/free-solid-svg-icons' import { faStar } from '@fortawesome/free-solid-svg-icons'
import { import { faStar as faStarRegular } from '@fortawesome/free-regular-svg-icons'
faStar as faStarRegular
} from '@fortawesome/free-regular-svg-icons'
library.add( library.add(faStar, faStarRegular)
faStar,
faStarRegular
)
const FavoriteButton = { const FavoriteButton = {
props: ['status', 'loggedIn'], props: ['status', 'loggedIn'],
data () { data() {
return { return {
animated: false animated: false
} }
}, },
methods: { methods: {
favorite () { favorite() {
if (!this.status.favorited) { if (!this.status.favorited) {
this.$store.dispatch('favorite', { id: this.status.id }) this.$store.dispatch('favorite', { id: this.status.id })
} else { } else {
@ -32,8 +27,10 @@ const FavoriteButton = {
}, },
computed: { computed: {
...mapGetters(['mergedConfig']), ...mapGetters(['mergedConfig']),
remoteInteractionLink () { remoteInteractionLink() {
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id }) return this.$store.getters.remoteInteractionLink({
statusId: this.status.id
})
} }
} }
} }

View file

@ -35,7 +35,7 @@
</div> </div>
</template> </template>
<script src="./favorite_button.js" ></script> <script src="./favorite_button.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
@ -56,6 +56,7 @@
.interactive { .interactive {
.svg-inline--fa { .svg-inline--fa {
animation-duration: 0.6s; animation-duration: 0.6s;
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
} }
&:hover .svg-inline--fa, &:hover .svg-inline--fa,

View file

@ -2,10 +2,20 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for
const FeaturesPanel = { const FeaturesPanel = {
computed: { computed: {
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled }, whoToFollow: function () {
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable }, return this.$store.state.instance.suggestionsEnabled
textlimit: function () { return this.$store.state.instance.textlimit }, },
uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) } mediaProxy: function () {
return this.$store.state.instance.mediaProxyAvailable
},
textlimit: function () {
return this.$store.state.instance.textlimit
},
uploadlimit: function () {
return fileSizeFormatService.fileSizeFormat(
this.$store.state.instance.uploadlimit
)
}
} }
} }

View file

@ -16,17 +16,20 @@
</li> </li>
<li>{{ $t('features_panel.scope_options') }}</li> <li>{{ $t('features_panel.scope_options') }}</li>
<li>{{ $t('features_panel.text_limit') }} = {{ textlimit }}</li> <li>{{ $t('features_panel.text_limit') }} = {{ textlimit }}</li>
<li>{{ $t('features_panel.upload_limit') }} = {{ uploadlimit.num }} {{ $t('upload.file_size_units.' + uploadlimit.unit) }}</li> <li>
{{ $t('features_panel.upload_limit') }} = {{ uploadlimit.num }}
{{ $t('upload.file_size_units.' + uploadlimit.unit) }}
</li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script src="./features_panel.js" ></script> <script src="./features_panel.js"></script>
<style lang="scss"> <style lang="scss">
.features-panel li { .features-panel li {
line-height: 24px; line-height: 24px;
} }
</style> </style>

View file

@ -5,14 +5,11 @@ import {
faExclamationTriangle faExclamationTriangle
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faStop, faExclamationTriangle)
faStop,
faExclamationTriangle
)
const Flash = { const Flash = {
props: [ 'src' ], props: ['src'],
data () { data() {
return { return {
player: false, // can be true, "hidden", false. hidden = element exists player: false, // can be true, "hidden", false. hidden = element exists
loaded: false, loaded: false,
@ -20,7 +17,7 @@ const Flash = {
} }
}, },
methods: { methods: {
openPlayer () { openPlayer() {
if (this.player) return // prevent double-loading, or re-loading on failure if (this.player) return // prevent double-loading, or re-loading on failure
this.player = 'hidden' this.player = 'hidden'
RuffleService.getRuffle().then((ruffle) => { RuffleService.getRuffle().then((ruffle) => {
@ -32,17 +29,20 @@ const Flash = {
container.appendChild(player) container.appendChild(player)
player.style.width = '100%' player.style.width = '100%'
player.style.height = '100%' player.style.height = '100%'
player.load(this.src).then(() => { player
this.player = true .load(this.src)
}).catch((e) => { .then(() => {
console.error('Error loading ruffle', e) this.player = true
this.player = 'error' })
}) .catch((e) => {
console.error('Error loading ruffle', e)
this.player = 'error'
})
this.ruffleInstance = player this.ruffleInstance = player
this.$emit('playerOpened') this.$emit('playerOpened')
}) })
}, },
closePlayer () { closePlayer() {
this.ruffleInstance && this.ruffleInstance.remove() this.ruffleInstance && this.ruffleInstance.remove()
this.player = false this.player = false
this.$emit('playerClosed') this.$emit('playerClosed')

View file

@ -1,24 +1,27 @@
import ConfirmModal from '../confirm_modal/confirm_modal.vue' import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import {
requestFollow,
requestUnfollow
} from '../../services/follow_manipulate/follow_manipulate'
export default { export default {
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'], props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
components: { components: {
ConfirmModal ConfirmModal
}, },
data () { data() {
return { return {
inProgress: false, inProgress: false,
showingConfirmUnfollow: false showingConfirmUnfollow: false
} }
}, },
computed: { computed: {
shouldConfirmUnfollow () { shouldConfirmUnfollow() {
return this.$store.getters.mergedConfig.modalOnUnfollow return this.$store.getters.mergedConfig.modalOnUnfollow
}, },
isPressed () { isPressed() {
return this.inProgress || this.relationship.following return this.inProgress || this.relationship.following
}, },
title () { title() {
if (this.inProgress || this.relationship.following) { if (this.inProgress || this.relationship.following) {
return this.$t('user_card.follow_unfollow') return this.$t('user_card.follow_unfollow')
} else if (this.relationship.requested) { } else if (this.relationship.requested) {
@ -27,7 +30,7 @@ export default {
return this.$t('user_card.follow') return this.$t('user_card.follow')
} }
}, },
label () { label() {
if (this.inProgress) { if (this.inProgress) {
return this.$t('user_card.follow_progress') return this.$t('user_card.follow_progress')
} else if (this.relationship.following) { } else if (this.relationship.following) {
@ -38,39 +41,44 @@ export default {
return this.$t('user_card.follow') return this.$t('user_card.follow')
} }
}, },
disabled () { disabled() {
return this.inProgress || this.user.deactivated return this.inProgress || this.user.deactivated
} }
}, },
methods: { methods: {
showConfirmUnfollow () { showConfirmUnfollow() {
this.showingConfirmUnfollow = true this.showingConfirmUnfollow = true
}, },
hideConfirmUnfollow () { hideConfirmUnfollow() {
this.showingConfirmUnfollow = false this.showingConfirmUnfollow = false
}, },
onClick () { onClick() {
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow() this.relationship.following || this.relationship.requested
? this.unfollow()
: this.follow()
}, },
follow () { follow() {
this.inProgress = true this.inProgress = true
requestFollow(this.relationship.id, this.$store).then(() => { requestFollow(this.relationship.id, this.$store).then(() => {
this.inProgress = false this.inProgress = false
}) })
}, },
unfollow () { unfollow() {
if (this.shouldConfirmUnfollow) { if (this.shouldConfirmUnfollow) {
this.showConfirmUnfollow() this.showConfirmUnfollow()
} else { } else {
this.doUnfollow() this.doUnfollow()
} }
}, },
doUnfollow () { doUnfollow() {
const store = this.$store const store = this.$store
this.inProgress = true this.inProgress = true
requestUnfollow(this.relationship.id, store).then(() => { requestUnfollow(this.relationship.id, store).then(() => {
this.inProgress = false this.inProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id }) store.commit('removeStatus', {
timeline: 'friends',
userId: this.relationship.id
})
}) })
this.hideConfirmUnfollow() this.hideConfirmUnfollow()

View file

@ -21,9 +21,7 @@
tag="span" tag="span"
> >
<template #user> <template #user>
<span <span v-text="user.screen_name_ui" />
v-text="user.screen_name_ui"
/>
</template> </template>
</i18n-t> </i18n-t>
</confirm-modal> </confirm-modal>

View file

@ -4,10 +4,7 @@ import FollowButton from '../follow_button/follow_button.vue'
import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue' import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue'
const FollowCard = { const FollowCard = {
props: [ props: ['user', 'noFollowsYou'],
'user',
'noFollowsYou'
],
components: { components: {
BasicUserCard, BasicUserCard,
RemoteFollow, RemoteFollow,
@ -15,13 +12,13 @@ const FollowCard = {
RemoveFollowerButton RemoveFollowerButton
}, },
computed: { computed: {
isMe () { isMe() {
return this.$store.state.users.currentUser.id === this.user.id return this.$store.state.users.currentUser.id === this.user.id
}, },
loggedIn () { loggedIn() {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
}, },
relationship () { relationship() {
return this.$store.getters.relationship(this.user.id) return this.$store.getters.relationship(this.user.id)
} }
} }

View file

@ -8,78 +8,90 @@ const FollowRequestCard = {
BasicUserCard, BasicUserCard,
ConfirmModal ConfirmModal
}, },
data () { data() {
return { return {
showingApproveConfirmDialog: false, showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false showingDenyConfirmDialog: false
} }
}, },
methods: { methods: {
findFollowRequestNotificationId () { findFollowRequestNotificationId() {
const notif = notificationsFromStore(this.$store).find( const notif = notificationsFromStore(this.$store).find(
(notif) => notif.from_profile.id === this.user.id && notif.type === 'follow_request' (notif) =>
notif.from_profile.id === this.user.id &&
notif.type === 'follow_request'
) )
return notif && notif.id return notif && notif.id
}, },
showApproveConfirmDialog () { showApproveConfirmDialog() {
this.showingApproveConfirmDialog = true this.showingApproveConfirmDialog = true
}, },
hideApproveConfirmDialog () { hideApproveConfirmDialog() {
this.showingApproveConfirmDialog = false this.showingApproveConfirmDialog = false
}, },
showDenyConfirmDialog () { showDenyConfirmDialog() {
this.showingDenyConfirmDialog = true this.showingDenyConfirmDialog = true
}, },
hideDenyConfirmDialog () { hideDenyConfirmDialog() {
this.showingDenyConfirmDialog = false this.showingDenyConfirmDialog = false
}, },
approveUser () { approveUser() {
if (this.shouldConfirmApprove) { if (this.shouldConfirmApprove) {
this.showApproveConfirmDialog() this.showApproveConfirmDialog()
} else { } else {
this.doApprove() this.doApprove()
} }
}, },
doApprove () { doApprove() {
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
this.$store.dispatch('decrementFollowRequestsCount')
const notifId = this.findFollowRequestNotificationId() const notifId = this.findFollowRequestNotificationId()
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId }) this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
this.$store.dispatch('updateNotification', { this.$store.dispatch('updateNotification', {
id: notifId, id: notifId,
updater: notification => { updater: (notification) => {
notification.type = 'follow' notification.type = 'follow'
} }
}) })
this.hideApproveConfirmDialog() this.hideApproveConfirmDialog()
}, },
denyUser () { denyUser() {
if (this.shouldConfirmDeny) { if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog() this.showDenyConfirmDialog()
} else { } else {
this.doDeny() this.doDeny()
} }
}, },
doDeny () { doDeny() {
const notifId = this.findFollowRequestNotificationId() const notifId = this.findFollowRequestNotificationId()
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) this.$store.state.api.backendInteractor
.denyUser({ id: this.user.id })
.then(() => { .then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: notifId }) this.$store.dispatch('dismissNotificationLocal', { id: notifId })
this.$store.dispatch('decrementFollowRequestsCount')
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
}) })
this.hideDenyConfirmDialog() this.hideDenyConfirmDialog()
} }
}, },
computed: { computed: {
mergedConfig () { mergedConfig() {
return this.$store.getters.mergedConfig return this.$store.getters.mergedConfig
}, },
shouldConfirmApprove () { shouldConfirmApprove() {
return this.mergedConfig.modalOnApproveFollow return this.mergedConfig.modalOnApproveFollow
}, },
shouldConfirmDeny () { shouldConfirmDeny() {
return this.mergedConfig.modalOnDenyFollow return this.mergedConfig.modalOnDenyFollow
},
show() {
const notifId = this.$store.state.api.followRequests.find(
(req) => req.id === this.user.id
)
return notifId !== undefined
} }
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -5,11 +5,9 @@ export default {
components: { components: {
Select Select
}, },
props: [ props: ['name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'],
'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'
],
emits: ['update:modelValue'], emits: ['update:modelValue'],
data () { data() {
return { return {
lValue: this.modelValue, lValue: this.modelValue,
availableOptions: [ availableOptions: [
@ -19,43 +17,45 @@ export default {
'serif', 'serif',
'monospace', 'monospace',
'sans-serif' 'sans-serif'
].filter(_ => _) ].filter((_) => _)
} }
}, },
beforeUpdate () { beforeUpdate() {
this.lValue = this.modelValue this.lValue = this.modelValue
}, },
computed: { computed: {
present () { present() {
return typeof this.lValue !== 'undefined' return typeof this.lValue !== 'undefined'
}, },
dValue () { dValue() {
return this.lValue || this.fallback || {} return this.lValue || this.fallback || {}
}, },
family: { family: {
get () { get() {
return this.dValue.family return this.dValue.family
}, },
set (v) { set(v) {
set(this.lValue, 'family', v) set(this.lValue, 'family', v)
this.$emit('update:modelValue', this.lValue) this.$emit('update:modelValue', this.lValue)
} }
}, },
isCustom () { isCustom() {
return this.preset === 'custom' return this.preset === 'custom'
}, },
preset: { preset: {
get () { get() {
if (this.family === 'serif' || if (
this.family === 'sans-serif' || this.family === 'serif' ||
this.family === 'monospace' || this.family === 'sans-serif' ||
this.family === 'inherit') { this.family === 'monospace' ||
this.family === 'inherit'
) {
return this.family return this.family
} else { } else {
return 'custom' return 'custom'
} }
}, },
set (v) { set(v) {
this.family = v === 'custom' ? '' : v this.family = v === 'custom' ? '' : v
} }
} }

View file

@ -15,8 +15,13 @@
class="opt exlcude-disabled" class="opt exlcude-disabled"
type="checkbox" type="checkbox"
:checked="present" :checked="present"
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" @change="
> $emit(
'update:modelValue',
typeof modelValue === 'undefined' ? fallback : undefined
)
"
/>
<label <label
v-if="typeof fallback !== 'undefined'" v-if="typeof fallback !== 'undefined'"
class="opt-l" class="opt-l"
@ -43,11 +48,11 @@
v-model="family" v-model="family"
class="custom-font" class="custom-font"
type="text" type="text"
> />
</div> </div>
</template> </template>
<script src="./font_control.js" ></script> <script src="./font_control.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';

View file

@ -4,7 +4,9 @@ const FriendsTimeline = {
Timeline Timeline
}, },
computed: { computed: {
timeline () { return this.$store.state.statuses.timelines.friends } timeline() {
return this.$store.state.statuses.timelines.friends
}
} }
} }

View file

@ -17,7 +17,7 @@ const Gallery = {
'editAttachment', 'editAttachment',
'grid' 'grid'
], ],
data () { data() {
return { return {
sizes: {}, sizes: {},
hidingLong: true hidingLong: true
@ -25,42 +25,61 @@ const Gallery = {
}, },
components: { Attachment }, components: { Attachment },
computed: { computed: {
rows () { rows() {
if (!this.attachments) { if (!this.attachments) {
return [] return []
} }
const attachments = this.limit > 0 const attachments =
? this.attachments.slice(0, this.limit) this.limit > 0
: this.attachments ? this.attachments.slice(0, this.limit)
: this.attachments
if (this.size === 'hide') { if (this.size === 'hide') {
return attachments.map(item => ({ minimal: true, items: [item] })) return attachments.map((item) => ({ minimal: true, items: [item] }))
} }
const rows = this.grid const rows = this.grid
? [{ grid: true, items: attachments }] ? [{ grid: true, items: attachments }]
: attachments.reduce((acc, attachment, i) => { : attachments
if (attachment.mimetype.includes('audio')) { .reduce(
return [...acc, { audio: true, items: [attachment] }, { items: [] }] (acc, attachment, i) => {
} if (attachment.mimetype.includes('audio')) {
if (!( return [
attachment.mimetype.includes('image') || ...acc,
attachment.mimetype.includes('video') || { audio: true, items: [attachment] },
attachment.mimetype.includes('flash') { items: [] }
)) { ]
return [...acc, { minimal: true, items: [attachment] }, { items: [] }] }
} if (
const maxPerRow = 3 !(
const attachmentsRemaining = this.attachments.length - i + 1 attachment.mimetype.includes('image') ||
const currentRow = acc[acc.length - 1].items attachment.mimetype.includes('video') ||
currentRow.push(attachment) attachment.mimetype.includes('flash')
if (currentRow.length >= maxPerRow && attachmentsRemaining > maxPerRow) { )
return [...acc, { items: [] }] ) {
} else { return [
return acc ...acc,
} { minimal: true, items: [attachment] },
}, [{ items: [] }]).filter(_ => _.items.length > 0) { items: [] }
]
}
const maxPerRow = 3
const attachmentsRemaining = this.attachments.length - i + 1
const currentRow = acc[acc.length - 1].items
currentRow.push(attachment)
if (
currentRow.length >= maxPerRow &&
attachmentsRemaining > maxPerRow
) {
return [...acc, { items: [] }]
} else {
return acc
}
},
[{ items: [] }]
)
.filter((_) => _.items.length > 0)
return rows return rows
}, },
attachmentsDimensionalScore () { attachmentsDimensionalScore() {
return this.rows.reduce((acc, row) => { return this.rows.reduce((acc, row) => {
let size = 0 let size = 0
if (row.minimal) { if (row.minimal) {
@ -73,7 +92,7 @@ const Gallery = {
return acc + size return acc + size
}, 0) }, 0)
}, },
tooManyAttachments () { tooManyAttachments() {
if (this.editable || this.size === 'small') { if (this.editable || this.size === 'small') {
return false return false
} else if (this.size === 'hide') { } else if (this.size === 'hide') {
@ -84,32 +103,32 @@ const Gallery = {
} }
}, },
methods: { methods: {
onNaturalSizeLoad ({ id, width, height }) { onNaturalSizeLoad({ id, width, height }) {
set(this.sizes, id, { width, height }) set(this.sizes, id, { width, height })
}, },
rowStyle (row) { rowStyle(row) {
if (row.audio) { if (row.audio) {
return { 'padding-bottom': '25%' } // fixed reduced height for audio return { 'padding-bottom': '25%' } // fixed reduced height for audio
} else if (!row.minimal && !row.grid) { } else if (!row.minimal && !row.grid) {
return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` } return { 'padding-bottom': `${100 / (row.items.length + 0.6)}%` }
} }
}, },
itemStyle (id, row) { itemStyle(id, row) {
const total = sumBy(row, item => this.getAspectRatio(item.id)) const total = sumBy(row, (item) => this.getAspectRatio(item.id))
return { flex: `${this.getAspectRatio(id) / total} 1 0%` } return { flex: `${this.getAspectRatio(id) / total} 1 0%` }
}, },
getAspectRatio (id) { getAspectRatio(id) {
const size = this.sizes[id] const size = this.sizes[id]
return size ? size.width / size.height : 1 return size ? size.width / size.height : 1
}, },
toggleHidingLong (event) { toggleHidingLong(event) {
this.hidingLong = event this.hidingLong = event
}, },
openGallery () { openGallery() {
this.$store.dispatch('setMedia', this.attachments) this.$store.dispatch('setMedia', this.attachments)
this.$store.dispatch('setCurrentMedia', this.attachments[0]) this.$store.dispatch('setCurrentMedia', this.attachments[0])
}, },
onMedia () { onMedia() {
this.$store.dispatch('setMedia', this.attachments) this.$store.dispatch('setMedia', this.attachments)
} }
} }

View file

@ -25,11 +25,20 @@
:size="size" :size="size"
:editable="editable" :editable="editable"
:remove="removeAttachment" :remove="removeAttachment"
:shift-up="!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment" :shift-up="
:shift-dn="!(attachmentIndex === row.items.length - 1 && rowIndex === rows.length - 1) && shiftDnAttachment" !(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment
"
:shift-dn="
!(
attachmentIndex === row.items.length - 1 &&
rowIndex === rows.length - 1
) && shiftDnAttachment
"
:edit="editAttachment" :edit="editAttachment"
:description="descriptions && descriptions[attachment.id]" :description="descriptions && descriptions[attachment.id]"
:hide-description="size === 'small' || tooManyAttachments && hidingLong" :hide-description="
size === 'small' || (tooManyAttachments && hidingLong)
"
:style="itemStyle(attachment.id, row.items)" :style="itemStyle(attachment.id, row.items)"
@setMedia="onMedia" @setMedia="onMedia"
@naturalSizeLoad="onNaturalSizeLoad" @naturalSizeLoad="onNaturalSizeLoad"
@ -42,7 +51,7 @@
class="many-attachments" class="many-attachments"
> >
<div class="many-attachments-text"> <div class="many-attachments-text">
{{ $t("status.many_attachments", { number: attachments.length }) }} {{ $t('status.many_attachments', { number: attachments.length }) }}
</div> </div>
<div class="many-attachments-buttons"> <div class="many-attachments-buttons">
<span <span
@ -53,7 +62,7 @@
class="button-unstyled -link" class="button-unstyled -link"
@click="toggleHidingLong(true)" @click="toggleHidingLong(true)"
> >
{{ $t("status.collapse_attachments") }} {{ $t('status.collapse_attachments') }}
</button> </button>
</span> </span>
<span <span
@ -64,7 +73,7 @@
class="button-unstyled -link" class="button-unstyled -link"
@click="toggleHidingLong(false)" @click="toggleHidingLong(false)"
> >
{{ $t("status.show_all_attachments") }} {{ $t('status.show_all_attachments') }}
</button> </button>
</span> </span>
<span <span
@ -75,7 +84,7 @@
class="button-unstyled -link" class="button-unstyled -link"
@click="openGallery" @click="openGallery"
> >
{{ $t("status.open_gallery") }} {{ $t('status.open_gallery') }}
</button> </button>
</span> </span>
</div> </div>
@ -83,7 +92,7 @@
</div> </div>
</template> </template>
<script src='./gallery.js'></script> <script src="./gallery.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
@ -109,8 +118,8 @@
.gallery-rows { .gallery-rows {
max-height: 25em; max-height: 25em;
overflow: hidden; overflow: hidden;
mask: mask: linear-gradient(to top, white, transparent) bottom/100% 70px
linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat, no-repeat,
linear-gradient(to top, white, white); linear-gradient(to top, white, white);
/* Autoprefixed seem to ignore this one, and also syntax is different */ /* Autoprefixed seem to ignore this one, and also syntax is different */

View file

@ -1,20 +1,16 @@
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import { faTimes } from '@fortawesome/free-solid-svg-icons'
faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add( library.add(faTimes)
faTimes
)
const GlobalNoticeList = { const GlobalNoticeList = {
computed: { computed: {
notices () { notices() {
return this.$store.state.interface.globalNotices return this.$store.state.interface.globalNotices
} }
}, },
methods: { methods: {
closeNotice (notice) { closeNotice(notice) {
this.$store.dispatch('removeGlobalNotice', notice) this.$store.dispatch('removeGlobalNotice', notice)
} }
} }

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