Compare commits

...

154 commits

Author SHA1 Message Date
4a50b1273d Merge pull request 'fix panel z-index conflicting with heading popover' (#422) from tea/akkoma-fe:fix/panel-z-index into develop
Reviewed-on: AkkomaGang/akkoma-fe#422
2024-10-26 03:42:48 +00:00
c76dc6d79e Merge pull request 'Fix redirect on logout' (#420) from Oneric/akkoma-fe:logout-redirect into develop
Reviewed-on: AkkomaGang/akkoma-fe#420
2024-10-26 03:42:23 +00:00
cb4c581cde Merge pull request 'Add proper autocomplete prop for TOTP login field' (#424) from tudbut/akkoma-fe:develop into develop
Reviewed-on: AkkomaGang/akkoma-fe#424
2024-10-26 03:41:15 +00:00
TudbuT
8231c8f0b6
add proper autocomplete prop for TOTP login field 2024-10-19 19:19:15 +02:00
tea
35cf3327c8 fix panel z-index conflicting with heading popover
resolves #342
2024-10-05 10:59:46 +02:00
1ae09458c6 Fix redirect on logout
An instance may restrict access to the public timeline (among others)
to authenticated users and there already is a setting to decide which page
to show authenticated and unauthenticated viewers by default each.
However, the logout redirect didn't honour this setting
leading to potentially broken pages and errors on logout
2024-09-28 17:47:28 +02:00
f391cf70a4 Merge pull request 'README: Add line to install Node.js' (#409) from ilja/akkoma-fe:README_add_to_install_node into develop
Reviewed-on: AkkomaGang/akkoma-fe#409
2024-08-25 09:09:35 +00:00
fa8fde2ab1 Merge pull request 'Upgrade vue packages' (#411) from Oneric/akkoma-fe:vue-mathml into develop
Reviewed-on: AkkomaGang/akkoma-fe#411
2024-08-25 09:08:04 +00:00
1f2c96a485 Merge pull request 'Fix setting restore from file' (#406) from Oneric/akkoma-fe:fix-file-restore into develop
Reviewed-on: AkkomaGang/akkoma-fe#406
2024-08-25 09:07:18 +00:00
6c178aa257 Upgrade vue packages
Bumping past vue 3.4.0 guarantees MathML support for FEP-dc88.

Related to: AkkomaGang/akkoma#642
2024-08-17 18:01:59 +02:00
ilja
3349fe6ff2 Add line to install Node.js
For someone who isn't used to building fe things like this, it's
not always clear to install Node.js or what version.

A line has been added to the installation instructions pointing to
resources where to get it and what version to use. For version I
point to the woodpecker config because that is what the CI uses and
therefor always "tested".

There was a file .node-version who contained a node version, but
this was seriously outdated and removing it didn't seem to break
anything. Assuming it indeed doesn't do anything any more, it seems
better to remove to avoid confusion.
2024-08-03 09:54:14 +02:00
e274adf47d Fix setting restore from file
Previously restoring from file would also restore the old version value
breaking upload of the new settings to the server.

Additionallly it didn’t even attempt to sync settings after restore and
was insufferably slow due to individually updating every single setting
with a dispatch. Instead only update changed settings like on server
syncs which usually speeds the process up considerably.

Fixes: AkkomaGang/akkoma-fe#405
2024-07-06 01:59:42 +02:00
8765491399 Do not try to destructure when we don't need to 2024-06-27 02:58:52 +01:00
4211e05a75 Merge pull request 'status component: Fix repeater name overflowing' (#383) from yukijoou/akkoma-fe:fix-status-usernames into develop
Reviewed-on: AkkomaGang/akkoma-fe#383
2024-06-25 21:34:15 +00:00
a3251a1ba6 Merge remote-tracking branch 'origin/translations' into develop 2024-06-23 03:03:40 +01:00
e5608f4009 remove ora as a dep 2024-06-23 03:03:11 +01:00
1092d43802 remove nonsense dep 2024-06-23 03:02:45 +01:00
Weblate
98a3622172 Merge branch 'origin/develop' into Weblate. 2024-06-17 21:40:59 +00:00
24b9e350e2 Merge pull request 'added minimum space to empty timeline' (#400) from Riedler/akkoma-fe:empty-tl-minspace into develop
Reviewed-on: AkkomaGang/akkoma-fe#400
2024-06-17 21:40:56 +00:00
Weblate
7ab4d22a4c Merge branch 'origin/develop' into Weblate. 2024-06-17 21:40:29 +00:00
8489f6d5ae Merge pull request 'visually fuse CW line and post body textarea' (#401) from Riedler/akkoma-fe:fuse-cw-to-post-body into develop
Reviewed-on: AkkomaGang/akkoma-fe#401
2024-06-17 21:40:26 +00:00
b2cab6d088 only flatten top of post body textarea if subject line is visible 2024-06-16 16:26:44 +02:00
3ebaba6fa7 smushed subject line and post body together, kinda 2024-06-16 16:14:16 +02:00
f1058567b9 also set min height for other lists (e.g. follow requests), not just the timeline 2024-06-16 16:12:15 +02:00
49a850a1e9 added minimum space to empty timeline 2024-06-16 15:49:52 +02:00
Weblate
b2de68239f Merge branch 'origin/develop' into Weblate. 2024-06-15 12:41:26 +00:00
c68595345f Merge pull request 'ANNOYING dependency update' (#397) from dep-update into develop
Reviewed-on: AkkomaGang/akkoma-fe#397
2024-06-15 12:41:23 +00:00
a5d4b0a68c Merge branch 'develop' into dep-update 2024-06-15 13:38:40 +01:00
bd263587b2 Merge branch 'develop' into dep-update 2024-06-15 13:36:24 +01:00
Weblate
c19fb198ca Merge branch 'origin/develop' into Weblate. 2024-06-15 12:33:42 +00:00
97966045cb Merge pull request 'make status form selectors justified properly' (#398) from rat/akkoma-fe:status-form-selector into develop
Reviewed-on: AkkomaGang/akkoma-fe#398
2024-06-15 12:33:38 +00:00
rat
aad023c8a0 make status form selectors justified properly 2024-06-10 17:20:51 -07:00
c952b2335c correct eslint plugin 2024-05-29 04:04:56 +01:00
0baf31f498 correct package.json lint task 2024-05-29 04:01:29 +01:00
8fa24d0c40 migrate to eslint 9 syntax 2024-05-29 03:59:37 +01:00
5848c18ec8 remove unused CI file 2024-05-29 03:43:41 +01:00
Weblate
54dbead22c Merge branch 'origin/develop' into Weblate. 2024-05-28 21:33:19 +00:00
3e86db24d3 Update .woodpecker.yml 2024-05-28 21:33:15 +00:00
Weblate
d8f3f5002f Merge branch 'origin/develop' into Weblate. 2024-05-28 21:31:51 +00:00
7789c5def6 Update .woodpecker.yml 2024-05-28 21:31:46 +00:00
a45f482c79 remove a useless log 2024-05-28 04:18:32 +01:00
Weblate
ed22c480f9 Merge branch 'origin/develop' into Weblate. 2024-05-28 03:17:19 +00:00
f3934afbd8 make sure we normalise interfaceLanguage 2024-05-28 04:17:04 +01:00
Weblate
3797495e53 Merge branch 'origin/develop' into Weblate. 2024-05-28 03:15:47 +00:00
0b437ab6fd remove line left over in conflict 2024-05-28 04:15:35 +01:00
a7dea2f70f ANNOYING dependency update 2024-05-28 04:02:17 +01:00
Weblate
2c9da4a58c Merge branch 'origin/develop' into Weblate. 2024-05-28 02:25:05 +00:00
8964dce609 Merge pull request 'Make animated emojis in reactions pause' (#378) from sarayalth/akkoma-fe:pause-animated-reaction into develop
Reviewed-on: AkkomaGang/akkoma-fe#378
2024-05-28 02:25:02 +00:00
Weblate
156b036caa Merge branch 'origin/develop' into Weblate. 2024-05-28 02:24:09 +00:00
1a49a1b3ac Merge pull request 'Expand Unicode emoji map' (#385) from Oneric/akkoma-fe:emoji_update into develop
Reviewed-on: AkkomaGang/akkoma-fe#385
2024-05-28 02:24:06 +00:00
Weblate
61d82a2a07 Merge branch 'origin/develop' into Weblate. 2024-05-28 02:22:11 +00:00
1adef56603 Merge remote-tracking branch 'partizan/386-default-post-lang' into develop 2024-05-28 03:22:03 +01:00
Weblate
b9bf0f0002 Merge branch 'origin/develop' into Weblate. 2024-05-28 02:14:58 +00:00
5aaa605df0 add asdf tool file 2024-05-28 03:14:50 +01:00
Weblate
7136ea80b9 Merge branch 'origin/develop' into Weblate. 2024-05-28 02:14:48 +00:00
71e287d56c update CI to v2 2024-05-28 03:14:37 +01:00
Weblate
a64cdda725 Merge branch 'origin/develop' into Weblate. 2024-05-28 02:10:18 +00:00
70275684bf Merge pull request 'Fix posting for "special" interface languages' (#377) from Oneric/akkoma-fe:post-language-specialcodes into develop
Reviewed-on: AkkomaGang/akkoma-fe#377
2024-05-28 02:10:15 +00:00
Weblate
7e7f03aece Merge branch 'origin/develop' into Weblate. 2024-05-28 02:07:18 +00:00
29ff2ef455 Merge pull request 'Fix ordering of favourites timeline' (#392) from Oneric/akkoma-fe:favourite-ordering into develop
Reviewed-on: AkkomaGang/akkoma-fe#392
2024-05-28 02:07:15 +00:00
8c49474dea status component: Fix repeater name overflowing
If someone repeating a post had a long username, their username would
overflow beyond the bounds of the post.

This fixes this isse by turning the bar displaying the username and
repeat icon into a flexbox.
2024-05-18 01:25:15 +02:00
62e0dd858c Fix ordering of favourites timeline
The backend returns them order by when the post was favourited;
reordering them by post date jumbles everything up each addition
and serves no purpose.

Fixes: AkkomaGang/akkoma-fe#391
2024-05-15 18:47:47 +02:00
cc709394c5 remove unused classes on notifications 2024-05-14 18:09:21 +02:00
57beea6a0d fix: Use label and key for options 2024-05-13 16:13:58 +03:00
Weblate
45524552a0 Translated using Weblate (Vietnamese)
Currently translated at 91.9% (964 of 1048 strings)

Translated using Weblate (Vietnamese)

Currently translated at 92.2% (965 of 1046 strings)

Translated using Weblate (Vietnamese)

Currently translated at 92.2% (965 of 1046 strings)

Translated using Weblate (Vietnamese)

Currently translated at 84.3% (882 of 1046 strings)

Translated using Weblate (Vietnamese)

Currently translated at 84.3% (882 of 1046 strings)

Translated using Weblate (Vietnamese)

Currently translated at 79.8% (835 of 1046 strings)

Translated using Weblate (Vietnamese)

Currently translated at 79.8% (835 of 1046 strings)

Co-authored-by: Nguyễn Gia Phong <cnx@loang.net>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: xarvos <huyngo@disroot.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/vi/
Translation: Pleroma fe/pleroma-fe
2024-04-29 14:09:37 +00:00
Weblate
ee66b69ab5 Translated using Weblate (Japanese)
Currently translated at 0.2% (3 of 1046 strings)

Added translation using Weblate (Japanese)

Co-authored-by: Nakaya <s_fpfb_sub-second@yahoo.co.jp>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ja/
Translation: Pleroma fe/pleroma-fe
2024-04-29 14:09:37 +00:00
Weblate
d42e374704 Translated using Weblate (Greek)
Currently translated at 15.9% (167 of 1046 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: getimiskon <getimiskon@disroot.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/el/
Translation: Pleroma fe/pleroma-fe
2024-04-29 14:09:37 +00:00
Weblate
ce8a9d2b4a Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1048 of 1048 strings)

Merge branch 'origin/develop' into Weblate.

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1046 of 1046 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
2024-04-29 14:09:37 +00:00
Weblate
d2b7ac6d8c Translated using Weblate (Polish)
Currently translated at 99.7% (1045 of 1048 strings)

Translated using Weblate (Polish)

Currently translated at 99.7% (1045 of 1048 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (1046 of 1046 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (1046 of 1046 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: subtype <subtype@hollow.capital>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/pl/
Translation: Pleroma fe/pleroma-fe
2024-04-29 14:09:37 +00:00
Weblate
754c72cb24 Translated using Weblate (Portuguese)
Currently translated at 100.0% (1048 of 1048 strings)

Co-authored-by: Jammer Lammer <akHarINlMYExpSmVPDRT@proton.me>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/pt/
Translation: Pleroma fe/pleroma-fe
2024-04-29 14:09:37 +00:00
Weblate
f5bd195422 Translated using Weblate (Russian)
Currently translated at 68.7% (719 of 1046 strings)

Co-authored-by: Mel <hi@mel.gg>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ru/
Translation: Pleroma fe/pleroma-fe
2024-04-29 14:09:37 +00:00
Weblate
d49fd46554 Translated using Weblate (Japanese (ja_EASY))
Currently translated at 72.3% (757 of 1046 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
2024-04-29 14:09:37 +00:00
Weblate
9982373853 Translated using Weblate (Italian)
Currently translated at 80.4% (841 of 1045 strings)

Translated using Weblate (Italian)

Currently translated at 65.3% (683 of 1045 strings)

Co-authored-by: Cuche <cuche@mailbox.org>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/it/
Translation: Pleroma fe/pleroma-fe
2024-04-29 14:09:37 +00:00
Weblate
5206b5cf9c Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (1048 of 1048 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 99.2% (1040 of 1048 strings)

Co-authored-by: Toot <toothpicker@users.noreply.translate.akkoma.dev>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/zh_Hant/
Translation: Pleroma fe/pleroma-fe
2024-04-29 14:09:37 +00:00
Weblate
a65a06ca04 Translated using Weblate (Catalan)
Currently translated at 100.0% (1048 of 1048 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fadelkon <fadelkon@posteo.net>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/ca/
Translation: Pleroma fe/pleroma-fe
2024-04-29 14:09:37 +00:00
Weblate
c10b38afbc Translated using Weblate (Indonesian)
Currently translated at 71.9% (753 of 1046 strings)

Co-authored-by: Aldiantoro Nugroho <kriwil@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/id/
Translation: Pleroma fe/pleroma-fe
2024-04-29 14:09:37 +00:00
Weblate
009941ea2c Translated using Weblate (Spanish)
Currently translated at 93.7% (983 of 1048 strings)

Translated using Weblate (Spanish)

Currently translated at 93.9% (983 of 1046 strings)

Translated using Weblate (Spanish)

Currently translated at 92.5% (967 of 1045 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
2024-04-29 14:09:37 +00:00
042e8c78dc feat: Add "Default post language" option
Refs #386
2024-04-20 16:07:03 +03:00
0e07d88afa Expand Unicode emoji map
This pulls in 267 new emoji:
  - all 258 non-deprecated country or macro region
    flags (composed by two regional indicators)
  - all 3 regional flags currently recommended for general use
    (Wales, Scotland, England)
  - a few random ones i picked out
    - goose
    - heart on fire
    - heart mending
    - transgender flag
    - rainbow flag
    - pirate flag

The new names are derived from official Unicode names
with minor modifications to fit into the usual shortcode scheme
and dropping the flag_ prefix from country indicators.
Due to a naming conflict the old "japan" emoji
U+1F5FE SILHOUETTE OF JAPAN was renamed to "japan_silhouette".
2024-04-04 21:52:33 +02:00
1f5f8665c8 make animated reactions pause unless hovered on notifications 2024-03-01 20:02:19 +01:00
428ed70b0d Fix posting for special interface languages
Easy Japanses (ja_easy) and traditional Chinses (zh_Hant) use
(custom) non-ISO codes in the interface. Because MastoAPI only accepts
ISO 639 codes, the backend will return an error rendering users
unable to do anything unless the post’s language was explicitly set.
2024-02-26 08:05:55 +01:00
ed0b403c33 Merge pull request 'Auto-approve followbacks (frontend part)' (#365) from Oneric/akkoma-fe:followbacks-fe into develop
Reviewed-on: AkkomaGang/akkoma-fe#365
2024-02-20 16:24:37 +00:00
0f842b300b Merge pull request 'Display profile background of other users' (#371) from Oneric/akkoma-fe:profile-backgrounds into develop
Reviewed-on: AkkomaGang/akkoma-fe#371
2024-02-20 16:20:14 +00:00
865cb6f96a Merge pull request 'Add Indonesian translation' (#366) from leap123/akkoma-fe:leap123-patch-1 into develop
Reviewed-on: AkkomaGang/akkoma-fe#366
2024-02-19 14:04:34 +00:00
050c7df2e6 Display profile background of other users
And add a new frontend setting to hide other people's background.
2024-02-14 18:44:57 +01:00
a77a9e04d9 Expose new server-side permit_followback setting
Added to backend in AkkomaGang/akkoma#674
2024-02-04 22:19:14 +01:00
a57334991e Add Indonesian translation
The Indonesian translation is technically almost complete, just not added to messages.js
2024-01-19 04:27:26 +00:00
8dce31d0ad Merge pull request 'Improve UX of subject / Content Warning field' (#362) from hazelnoot/akkoma-fe:develop into develop
Reviewed-on: AkkomaGang/akkoma-fe#362
2023-12-20 18:49:40 +00:00
ea9ad4d600 fix "always show content warning" setting 2023-12-20 12:39:31 -05:00
34e2800f59 add button to toggle the spoiler / CW field 2023-12-16 14:44:26 -05:00
3d65eccf04 use main emoji button for spoiler / CW field 2023-12-16 13:37:59 -05:00
d304be654f Merge pull request 'Update build setup instructions' (#343) from norm/pleroma-fe:update-build-setup into develop
Reviewed-on: AkkomaGang/akkoma-fe#343
2023-12-15 12:24:33 +00:00
aee97fa948 Merge pull request 'Re-added extension checking for still-image' (#346) from Mergan/pleroma-fe:still-image-ultimate into develop
Reviewed-on: AkkomaGang/akkoma-fe#346
2023-12-15 12:24:07 +00:00
7da1687f31 Merge pull request 'Use relative font size and set appropriate overflow behavior' (#355) from xarvos/pleroma-fe:update-css into develop
Reviewed-on: AkkomaGang/akkoma-fe#355
2023-12-15 12:12:28 +00:00
a8f193d4bd Merge pull request 'Stop constant movement of notifications due to changing timestamps' (#353) from Oneric/akkoma-fe:notification-writhing into develop
Reviewed-on: AkkomaGang/akkoma-fe#353
2023-12-15 11:57:47 +00:00
81c82e11bc Merge pull request 'Explicitly set SameSite attribute for cookies' (#352) from Oneric/akkoma-fe:cookie-samesite into develop
Reviewed-on: AkkomaGang/akkoma-fe#352
2023-12-15 11:54:15 +00:00
00cadce5b4 Merge pull request 'Format dates, times with window.navigator.language instead of UI i18n locale' (#354) from smitten/akkoma-fe:date-locale-fix-cherrypick into develop
Reviewed-on: AkkomaGang/akkoma-fe#354
2023-12-15 11:52:59 +00:00
40a08f279b Merge pull request 'Drop broken "@ symbol as icon" setting' (#359) from Oneric/akkoma-fe:at-icon into develop
Reviewed-on: AkkomaGang/akkoma-fe#359
2023-11-16 10:41:17 +00:00
c524a47e6f Drop broken "@ symbol as icon" setting
It was merged into pleroma-fe on 2022-02-03 in
76547fe66d and imported
into akkoma-fe on 2022-06-08 with the merge commit
f6cf509a04.

However, something went wrong in the merge and while the setting
and its infrastructure exist, it is never used anywhere and @ is
always displayed as text.

Given it existed in this broken state for nearly one and a half years,
never worked on akkoma-fe and no bugs were filed about this, it appears
nobody cares, so let’s just remove it.
2023-11-15 23:36:19 +01:00
235c734d37
Use overflow: auto for description
Previously it sets overflow: scroll, so there's an unnecessary
horizontal scroll.
overflow: auto only shows scrollbar when it overflows
2023-11-05 09:32:19 +07:00
deaef1d0b9
Use relative unit for font size 2023-11-05 09:21:01 +07:00
1b28ec3b72
Match UI i18n configuration to browser locales 2023-11-01 23:10:57 -04:00
c9dc8f00f9
Use window.navigator.language before interface i18n language 2023-10-30 23:56:53 -04:00
beee99e733 Stop notifications boxes from change size over time
Notifications about favourites and follows use .notification-right,
notifications about replies instead use .heading-right.

Previously only the former set a min-width, however the
chosen value of 3em was too small to fit the worst case.
As a consequence, when the timestamp text changes over time,
its element width changes, which may result in neighbouring text
(no longer) needing to wrap to a new line in turn changing the size
of the whole notification box pushing older notification boxes down/up.

These constant movements at the side of the screen can be quite
annoying and confusing when the cause cannot be immediately discerned.

Avoid this, by reserving enough space for any timestamp.

For English, the worst case is the five-character 'XXmin', since the
short identifier for minutes is the longest with three letters.
With two exceptions, all other current localisation also do not exceed
three letters in any short unit identifier up to days.
However, some localisations (e.g. Polish) additionally insert a space
between numerical value and unit. This matches SI recommendations
pushing the worst case to 6 characters.

6 characters will be sufficient for timestamps up to 3 weeks in all
languages (minus prev exceptions), which seems reasonable enough
as beyond this timestamps rarely change anyway.

The aforementioned exceptions being Vietnamese and Occitan,
but in the current localisation all or the relevant short unit
identifiers are identical to the long forms indicating this is
just due to incomplete translation.
Indeed, Vietnamese Wikipedia (read through machine translation) suggests
“ph” is commonly used as unit identifiers for minutes, but the current
localisation fully spells it out as “phút”.
2023-10-25 00:37:09 +02:00
ccb0ffdc8a Don't show direction in notification timestamps
Currently all notifications except follow-related once include
and explicit direction text. (It missing in follow notifs is due to an
omission in 804ba0cdb6b353e0c959c68f44c6a1316c0d6b10 which only added
the newly introduced with-direction to status-related notifs. Before,
presumably all notifs included direction text.)

But in the notification tray horizontal space is scarce
and notifs can already be assumed to only come from the past.
While it might not be too bad for the English localisation’s 4-letter
' ago' suffix, e.g. the Indonesian localisation’s ' yang lalu' needs
10 letters.

Thus instead of fixing the omission for follow-related notifs,
drop direction text from all notification timestamps.
2023-10-24 23:28:45 +02:00
ab250c2f3a Explicitly set SameSite attribute for cookies
Modern browsers start to tighten down on third-party access to cookies.
E.g. in current Firefox, a warning about the userLanguage cookie was
shown since it did not yet explicitly set the SameSite attribute and the
default is about to change.

The cookie name being referred to as BACKEND_LANGUAGE_COOKIE_NAME
suggests it should be readable by the actual Akkoma backend, which can
live at a different domain than akkoma-fe. Thus explicitly enable
sharing with third-party sites.

No warnings were shown for other cookies, so I assume
this was the only one not yet setting SameSite.
2023-10-19 01:05:59 +02:00
1de62fffcd
Update config.example.json link and example domain 2023-10-06 04:52:04 -04:00
306cea04a1
Use corepack in build instructions 2023-10-06 04:51:59 -04:00
d9e1bc4d99 Re-added extension checking for still-image
- Bonus refactoring
2023-10-02 15:29:54 -07:00
52b0b6f008 add VI to messages.js 2023-10-02 13:28:23 +01:00
8afbe5e3bc Merge pull request 'Making still-image better' (#341) from Mergan/pleroma-fe:still-image-ultimate into develop
Reviewed-on: AkkomaGang/akkoma-fe#341
2023-09-25 13:29:29 +00:00
58be48d164 Merge pull request 'Do not copy all emojis in recentEmoji getter' (#340) from sn0w/akkoma-fe:feature/optimize-recent-emojis into develop
Reviewed-on: AkkomaGang/akkoma-fe#340
2023-09-25 13:24:12 +00:00
1056b89fd1 Disabled aggressive matching for reduced motion (we search for gif now) 2023-09-12 04:32:01 -07:00
3e64d78d05 An oopsie 2023-09-12 04:17:28 -07:00
3947aafeba Aligning canvas to image 2023-09-12 04:08:47 -07:00
345934c2f3 Make label visible on avatar 2023-09-12 03:37:05 -07:00
42a13b0f1b Modify label 2023-09-12 03:35:58 -07:00
e13c4b6b85 Revamped still-image 2023-09-12 02:48:53 -07:00
6a1409e09b
Do not copy all emojis in recentEmoji getter 2023-09-03 16:19:06 +02:00
174f98b1cb don't die on my arm box please 2023-08-05 14:17:42 +01:00
ab146b67ec version 2023-08-05 13:29:44 +01:00
3b4208ea41 debounce emoji searching 2023-08-04 16:48:13 +01:00
856324fa26 Merge pull request 'Make favicon next to post username use Still-Image functionality' (#327) from Mergan/pleroma-fe:still-image-instance-favicon into develop
Reviewed-on: AkkomaGang/akkoma-fe#327
2023-08-04 15:09:56 +00:00
5a9322d2c7 Merge pull request 'StillImage: Improved animated image detection' (#335) from yukijoou/akkoma-fe:still-image-better-gif-detection into develop
Reviewed-on: AkkomaGang/akkoma-fe#335
2023-08-04 15:09:32 +00:00
Weblate
b52bfbcba0 Merge branch 'origin/develop' into Weblate. 2023-08-04 14:56:22 +00:00
2b05a738c9 Merge pull request 'Add replying info for redraft' (#332) from xarvos/pleroma-fe:fix-reply-redraft into develop
Reviewed-on: AkkomaGang/akkoma-fe#332
2023-08-04 14:56:20 +00:00
Weblate
fa5d31b793 Merge branch 'origin/develop' into Weblate. 2023-08-04 14:54:38 +00:00
tusooa
29cfdcbbcd Add load more to blocks/mutes 2023-08-04 15:54:04 +01:00
Weblate
5174b95918 Translated using Weblate (Greek)
Currently translated at 15.4% (162 of 1046 strings)

Added translation using Weblate (Greek)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: getimiskon <getimiskon@disroot.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/el/
Translation: Pleroma fe/pleroma-fe
2023-08-04 13:34:26 +00:00
Weblate
56528206b8 Translated using Weblate (Portuguese)
Currently translated at 62.7% (656 of 1046 strings)

Co-authored-by: Jonathan Soares <jontix@murena.io>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/pt/
Translation: Pleroma fe/pleroma-fe
2023-08-04 13:34:26 +00:00
Weblate
590380c084 Translated using Weblate (Thai)
Currently translated at 1.7% (18 of 1046 strings)

Added translation using Weblate (Thai)

Co-authored-by: Chanakan Mungtin <chanakan5591@chanakancloud.net>
Co-authored-by: Pongsatorn Paewsoongnern <akkoma@miraiverse.me>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/th/
Translation: Pleroma fe/pleroma-fe
2023-08-04 13:34:26 +00:00
Weblate
dfcbb3c1ec Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1046 of 1046 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-08-04 13:34:26 +00:00
Weblate
9b01c31283 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1046 of 1046 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-08-04 13:34:26 +00:00
Weblate
6be003b2f8 Translated using Weblate (French)
Currently translated at 98.3% (1029 of 1046 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-08-04 13:34:26 +00:00
Weblate
cc302fb0e2 Translated using Weblate (Japanese (ja_EASY))
Currently translated at 72.3% (757 of 1046 strings)

Translated using Weblate (Japanese (ja_EASY))

Currently translated at 71.9% (753 of 1046 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-08-04 13:34:26 +00:00
Weblate
c4e768f977 Translated using Weblate (Indonesian)
Currently translated at 67.8% (710 of 1046 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yonle <yonle@lecturify.net>
Translate-URL: http://translate.akkoma.dev/projects/akkoma/pleroma-fe/id/
Translation: Pleroma fe/pleroma-fe
2023-08-04 13:34:26 +00:00
Weblate
d8a7217335 Translated using Weblate (Turkish)
Currently translated at 9.7% (102 of 1046 strings)

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-08-04 13:34:26 +00:00
35d10ab9ff Merge pull request 'components: Honour 'prefers reduced motion' setting in many components' (#333) from yukijoou/akkoma-fe:fix-reduced-motion into develop
Reviewed-on: AkkomaGang/akkoma-fe#333
2023-08-04 13:34:23 +00:00
Yuki Joou
43d0a24547 StillImage: Improved animated image detection
This patch makes StillImage's animation detection return early in cases
where we can't detect the mimetype of the image. It also sets the image
as animated in those cases if the user agent wants reduced motion.

As reduced motion is an accessibility setting, I think it's best to use
a "better safe than sorry" approach, it's better to accidentally mark
something as animated that isn't than to have unblocked animations.
2023-08-03 16:34:28 +02:00
Yuki Joou
51ebe643d5 components: Honour 'prefers reduced motion' setting in many components
This helps accessibility for motion-sensitive people such as myself, and
can improve battery life in "battery saving" mode on most devices
2023-08-03 14:45:02 +02:00
7c14e1a5bd
Add replying info for redraft 2023-08-02 08:40:19 +07:00
David
0da0e2c814 Make favicon next to post username use Still-Image functionality 2023-07-24 01:08:11 -07:00
af97dd7484 Merge pull request 'docs: Update Pleroma references to Akkoma' (#318) from norm/pleroma-fe:update-docs into develop
Reviewed-on: AkkomaGang/akkoma-fe#318
2023-07-21 19:44:25 +00:00
28bf597443 Merge pull request '[workaround] fix: emoji picker not scrollable on ios' (#323) from SukinoVerse/akkoma-fe:fix/emoji-picker-not-scrollable-ios into develop
Reviewed-on: AkkomaGang/akkoma-fe#323
2023-07-21 19:43:14 +00:00
a249baea8c Merge pull request 'Make emoji reactions use Still-Image functionality' (#326) from Mergan/pleroma-fe:still-image-emoji-reactions into develop
Reviewed-on: AkkomaGang/akkoma-fe#326
2023-07-21 19:42:41 +00:00
a4e82f7886 Merge pull request '[feat] Support prefers-reduced-motion disabling auto-play of animated images (#324)' (#325) from Mergan/pleroma-fe:support-prefers-reduced-motion into develop
Reviewed-on: AkkomaGang/akkoma-fe#325
2023-07-21 19:37:43 +00:00
David
32dc55b07c Make emoji reactions use Still-Image functionality 2023-07-20 17:20:54 -07:00
David
91eab22d77 Simplified and fixed and/or oopsie 2023-07-20 17:18:59 -07:00
David
e2125c57d6 Turn on by default 2023-07-20 16:26:52 -07:00
David
e0a6418e91 Add prefers-reduced-motion support 2023-07-20 16:14:36 -07:00
SukinoVERSΞ
14ed359c33 fix: emoji picker not scrollable on ios 2023-07-11 22:16:37 +07:00
2d387e2eb4
update more images 2023-05-26 23:02:38 -04:00
57f70371a9
use better example emoji 2023-05-26 22:59:55 -04:00
8feffbcdf6
docs: Update Pleroma references to Akkoma 2023-05-26 22:49:29 -04:00
174 changed files with 9169 additions and 6167 deletions

View file

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

View file

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

View file

@ -1 +0,0 @@
7.2.1

1
.tool-versions Normal file
View file

@ -0,0 +1 @@
nodejs 20.12.2

View file

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

View file

@ -20,9 +20,11 @@ To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/ad
## Build Setup ## Build Setup
Make sure you have [Node.js](https://nodejs.org/) installed. You can check `/.woodpecker.yml` for which node version the Akkoma CI currently uses.
``` bash ``` bash
# install dependencies # install dependencies
npm install -g yarn corepack enable
yarn yarn
# serve with hot reload at localhost:8080 # serve with hot reload at localhost:8080
@ -37,7 +39,7 @@ npm run unit
# For Contributors: # For Contributors:
You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/config/local.example.json)) to enable some convenience dev options: You can create file `/config/local.json` (see [example](https://akkoma.dev/AkkomaGang/akkoma-fe/src/branch/develop/config/local.example.json)) to enable some convenience dev options:
* `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases. * `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases.
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode. * `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode.

View file

@ -1,36 +1,36 @@
// https://github.com/shelljs/shelljs // https://github.com/shelljs/shelljs
require('./check-versions')() require("./check-versions")();
require('shelljs/global') require("shelljs/global");
env.NODE_ENV = 'production' env.NODE_ENV = "production";
var path = require('path') var path = require("path");
var config = require('../config') var config = require("../config");
var ora = require('ora') var webpack = require("webpack");
var webpack = require('webpack') var webpackConfig = require("./webpack.prod.conf");
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 assetsPath = path.join(
spinner.start() 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);
webpack(webpackConfig, function (err, stats) { webpack(webpackConfig, function (err, stats) {
spinner.stop() if (err) throw err;
if (err) throw err process.stdout.write(
process.stdout.write(stats.toString({ stats.toString({
colors: true, colors: true,
modules: false, modules: false,
children: false, children: false,
chunks: false, chunks: false,
chunkModules: false chunkModules: false,
}) + '\n') }) + "\n",
}) );
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 491 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

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

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 530 B

View file

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

Before

Width:  |  Height:  |  Size: 3.3 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

31
eslint.config.js Normal file
View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@
} }
html { html {
font-size: 14px; font-size: 0.875rem;
// overflow-x: clip causes my browser's tab to crash with SIGILL lul // overflow-x: clip causes my browser's tab to crash with SIGILL lul
} }

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,7 +93,7 @@
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"
/> />

View file

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

View file

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

View file

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

View file

@ -14,7 +14,7 @@
:model-value="present" :model-value="present"
:disabled="disabled" :disabled="disabled"
class="opt" class="opt"
@update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" @update:model-value="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
/> />
<div class="input color-input-field"> <div class="input color-input-field">
<input <input
@ -46,7 +46,6 @@
</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'
@ -108,6 +107,7 @@ export default {
} }
} }
</script> </script>
<style lang="scss" src="./color_input.scss"></style>
<style lang="scss"> <style lang="scss">
.color-control { .color-control {

View file

@ -25,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

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@
class="btn button-default" class="btn button-default"
> >
{{ $t('domain_mute_card.unmute') }} {{ $t('domain_mute_card.unmute') }}
<template 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

@ -2,7 +2,7 @@
<Modal <Modal
v-if="isFormVisible" v-if="isFormVisible"
class="edit-form-modal-view" class="edit-form-modal-view"
@backdropClicked="closeModal" @backdrop-clicked="closeModal"
> >
<div class="edit-form-modal-panel panel"> <div class="edit-form-modal-panel panel">
<div class="panel-heading"> <div class="panel-heading">
@ -11,10 +11,10 @@
<PostStatusForm <PostStatusForm
class="panel-body" class="panel-body"
v-bind="params" v-bind="params"
@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

@ -43,7 +43,10 @@
: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"

View file

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

View file

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

View file

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

View file

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

View file

@ -14,12 +14,13 @@
<span <span
v-if="reaction.url !== null" v-if="reaction.url !== null"
> >
<img <StillImage
:src="reaction.url" :src="reaction.url"
:title="reaction.name" :title="reaction.name"
:alt="reaction.name"
class="reaction-emoji" class="reaction-emoji"
height="2.55em" height="2.55em"
> />
{{ reaction.count }} {{ reaction.count }}
</span> </span>
<span v-else> <span v-else>

View file

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

View file

@ -7,7 +7,7 @@
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
remove-padding remove-padding
> >
<template 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"
@ -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"

View file

@ -55,6 +55,9 @@
.interactive { .interactive {
.svg-inline--fa { .svg-inline--fa {
@media (prefers-reduced-motion: reduce) {
animation: unset;
}
animation-duration: 0.6s; animation-duration: 0.6s;
} }

View file

@ -1,5 +1,8 @@
<template> <template>
<basic-user-card :user="user" v-if="show"> <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

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

View file

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

View file

@ -2,7 +2,7 @@
<Modal <Modal
v-if="showing" v-if="showing"
class="media-modal-view" class="media-modal-view"
@backdropClicked="hideIfNotSwiped" @backdrop-clicked="hideIfNotSwiped"
> >
<SwipeClick <SwipeClick
v-if="type === 'image'" v-if="type === 'image'"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -118,13 +118,16 @@
/> />
</div> </div>
<EmojiInput <EmojiInput
v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)" v-if="subjectVisible"
ref="subject-emoji-input"
v-model="newStatus.spoilerText" v-model="newStatus.spoilerText"
enable-emoji-picker enable-emoji-picker
hide-emoji-button
:suggest="emojiSuggestor" :suggest="emojiSuggestor"
class="form-control" class="form-control"
> >
<input <input
ref="subject-input"
v-model="newStatus.spoilerText" v-model="newStatus.spoilerText"
type="text" type="text"
:placeholder="$t('post_status.content_warning')" :placeholder="$t('post_status.content_warning')"
@ -132,6 +135,7 @@
size="1" size="1"
class="form-post-subject" class="form-post-subject"
@input="onSubjectInput" @input="onSubjectInput"
@focus="focusSubjectInput()"
> >
</EmojiInput> </EmojiInput>
<i18n-t <i18n-t
@ -166,13 +170,14 @@
cols="1" cols="1"
:disabled="posting && !optimisticPosting" :disabled="posting && !optimisticPosting"
class="form-post-body" class="form-post-body"
:class="{ 'scrollable-form': !!maxHeight }" :class="{ 'scrollable-form': !!maxHeight, '-has-subject': subjectVisible }"
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)" @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
@keydown.meta.enter="postStatus($event, newStatus)" @keydown.meta.enter="postStatus($event, newStatus)"
@keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)" @keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
@input="resize" @input="resize"
@compositionupdate="resize" @compositionupdate="resize"
@paste="paste" @paste="paste"
@focus="focusStatusInput()"
/> />
<p <p
v-if="hasStatusLengthLimit" v-if="hasStatusLengthLimit"
@ -185,6 +190,7 @@
<div <div
v-if="!disableScopeSelector" v-if="!disableScopeSelector"
class="visibility-tray" class="visibility-tray"
:class="{ 'visibility-tray-edit': isEdit }"
> >
<scope-selector <scope-selector
v-if="!disableVisibilitySelector" v-if="!disableVisibilitySelector"
@ -195,7 +201,9 @@
/> />
<div <div
class="language-selector" class="format-selector-container">
<div
class="format-selector"
> >
<Select <Select
id="post-language" id="post-language"
@ -203,17 +211,17 @@
class="form-control" class="form-control"
> >
<option <option
v-for="language in isoLanguages" v-for="language in postLanguageOptions"
:key="language" :key="language.key"
:value="language" :value="language.value"
> >
{{ language }} {{ language.label }}
</option> </option>
</Select> </Select>
</div> </div>
<div <div
v-if="postFormats.length > 1" v-if="postFormats.length > 1"
class="text-format" class="text-format format-selector"
> >
<Select <Select
id="post-content-type" id="post-content-type"
@ -231,7 +239,7 @@
</div> </div>
<div <div
v-if="postFormats.length === 1 && postFormats[0] !== 'text/plain'" v-if="postFormats.length === 1 && postFormats[0] !== 'text/plain'"
class="text-format" class="text-format format-selector"
> >
<span class="only-format"> <span class="only-format">
{{ $t(`post_status.content_type["${postFormats[0]}"]`) }} {{ $t(`post_status.content_type["${postFormats[0]}"]`) }}
@ -239,6 +247,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<poll-form <poll-form
v-if="pollsAvailable" v-if="pollsAvailable"
ref="pollForm" ref="pollForm"
@ -276,6 +285,15 @@
> >
<FAIcon icon="poll-h" /> <FAIcon icon="poll-h" />
</button> </button>
<button
v-if="!disableSubject"
class="spoiler-icon button-unstyled"
:class="{ selected: subjectVisible }"
:title="$t('post_status.toggle_content_warning')"
@click="toggleSubjectVisible"
>
<FAIcon icon="eye-slash" />
</button>
</div> </div>
<button <button
v-if="posting" v-if="posting"
@ -446,6 +464,10 @@
align-items: baseline; align-items: baseline;
} }
.visibility-tray-edit {
justify-content: right;
}
.visibility-notice.edit-warning { .visibility-notice.edit-warning {
> :first-child { > :first-child {
margin-top: 0; margin-top: 0;
@ -456,7 +478,13 @@
} }
} }
.media-upload-icon, .poll-icon, .emoji-icon { .format-selector-container {
.format-selector {
display: inline-block;
}
}
.media-upload-icon, .poll-icon, .emoji-icon, .spoiler-icon {
font-size: 1.85em; font-size: 1.85em;
line-height: 1.1; line-height: 1.1;
flex: 1; flex: 1;
@ -499,6 +527,11 @@
.poll-icon { .poll-icon {
order: 3; order: 3;
justify-content: center;
}
.spoiler-icon {
order: 4;
justify-content: right; justify-content: right;
} }
@ -551,6 +584,11 @@
line-height: 1.85; line-height: 1.85;
} }
.form-post-subject {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.form-post-body { .form-post-body {
// TODO: make a resizable textarea component? // TODO: make a resizable textarea component?
box-sizing: content-box; // needed for easier computation of dynamic size box-sizing: content-box; // needed for easier computation of dynamic size
@ -563,6 +601,11 @@
min-height: calc(var(--post-line-height) * 1em); min-height: calc(var(--post-line-height) * 1em);
resize: none; resize: none;
&.-has-subject {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
&.scrollable-form { &.scrollable-form {
overflow-y: auto; overflow-y: auto;
} }

View file

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

View file

@ -8,13 +8,13 @@
remove-padding remove-padding
@show="focusInput" @show="focusInput"
> >
<template v-slot:content="{close}"> <template #content="{close}">
<EmojiPicker <EmojiPicker
:enable-sticker-picker="false" :enable-sticker-picker="false"
@emoji="addReaction($event, close)" @emoji="addReaction($event, close)"
/> />
</template> </template>
<template v-slot:trigger> <template #trigger>
<button <button
class="button-unstyled popover-trigger" class="button-unstyled popover-trigger"
:title="$t('tool_tip.add_reaction')" :title="$t('tool_tip.add_reaction')"

View file

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

View file

@ -74,6 +74,9 @@
.interactive { .interactive {
.svg-inline--fa { .svg-inline--fa {
@media (prefers-reduced-motion: reduce) {
animation: unset;
}
animation-duration: 0.6s; animation-duration: 0.6s;
} }

View file

@ -24,7 +24,7 @@
:items="items" :items="items"
:get-key="getKey" :get-key="getKey"
> >
<template v-slot:item="{item}"> <template #item="{item}">
<div <div
class="selectable-list-item-inner" class="selectable-list-item-inner"
:class="{ 'selectable-list-item-selected-inner': isSelected(item) }" :class="{ 'selectable-list-item-selected-inner': isSelected(item) }"
@ -41,7 +41,7 @@
/> />
</div> </div>
</template> </template>
<template v-slot:empty> <template #empty>
<slot name="empty" /> <slot name="empty" />
</template> </template>
</List> </List>

View file

@ -6,7 +6,7 @@
<Checkbox <Checkbox
:model-value="state" :model-value="state"
:disabled="disabled" :disabled="disabled"
@update:modelValue="update" @update:model-value="update"
> >
<span <span
v-if="!!$slots.default" v-if="!!$slots.default"

View file

@ -8,7 +8,7 @@
<Select <Select
:model-value="state" :model-value="state"
:disabled="disabled" :disabled="disabled"
@update:modelValue="update" @update:model-value="update"
> >
<option <option
v-for="option in options" v-for="option in options"

View file

@ -6,14 +6,14 @@
<Popover <Popover
trigger="hover" trigger="hover"
> >
<template v-slot:trigger> <template #trigger>
&nbsp; &nbsp;
<FAIcon <FAIcon
icon="wrench" icon="wrench"
:aria-label="$t('settings.setting_changed')" :aria-label="$t('settings.setting_changed')"
/> />
</template> </template>
<template v-slot:content> <template #content>
<div class="modified-tooltip"> <div class="modified-tooltip">
{{ $t('settings.setting_changed') }} {{ $t('settings.setting_changed') }}
</div> </div>

View file

@ -6,14 +6,14 @@
<Popover <Popover
trigger="hover" trigger="hover"
> >
<template v-slot:trigger> <template #trigger>
&nbsp; &nbsp;
<FAIcon <FAIcon
icon="server" icon="server"
:aria-label="$t('settings.setting_server_side')" :aria-label="$t('settings.setting_server_side')"
/> />
</template> </template>
<template v-slot:content> <template #content>
<div class="serverside-tooltip"> <div class="serverside-tooltip">
{{ $t('settings.setting_server_side') }} {{ $t('settings.setting_server_side') }}
</div> </div>

View file

@ -69,7 +69,7 @@ const SettingsModal = {
this.$store.dispatch('closeSettingsModal') this.$store.dispatch('closeSettingsModal')
}, },
logout () { logout () {
this.$router.replace('/main/public') this.$router.replace(this.$store.state.instance.redirectRootNoLogin || '/main/all')
this.$store.dispatch('closeSettingsModal') this.$store.dispatch('closeSettingsModal')
this.$store.dispatch('logout') this.$store.dispatch('logout')
}, },

View file

@ -108,7 +108,7 @@
<Checkbox <Checkbox
:model-value="!!expertLevel" :model-value="!!expertLevel"
class="expertMode" class="expertMode"
@update:modelValue="expertLevel = Number($event)" @update:model-value="expertLevel = Number($event)"
> >
{{ $t("settings.expert_mode") }} {{ $t("settings.expert_mode") }}
</Checkbox> </Checkbox>

View file

@ -72,7 +72,7 @@ const DataImportExportTab = {
// check is it's a local user // check is it's a local user
if (user && user.is_local) { if (user && user.is_local) {
// append the instance address // append the instance address
// eslint-disable-next-line no-undef
return user.screen_name + '@' + location.hostname return user.screen_name + '@' + location.hostname
} }
return user.screen_name return user.screen_name

View file

@ -4,6 +4,7 @@ import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
import IntegerSetting from '../helpers/integer_setting.vue' import IntegerSetting from '../helpers/integer_setting.vue'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import { usePostLanguageOptions } from 'src/lib/post_language'
import SharedComputedObject from '../helpers/shared_computed_object.js' import SharedComputedObject from '../helpers/shared_computed_object.js'
import ServerSideIndicator from '../helpers/server_side_indicator.vue' import ServerSideIndicator from '../helpers/server_side_indicator.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@ -17,6 +18,11 @@ library.add(
) )
const GeneralTab = { const GeneralTab = {
setup() {
const {postLanguageOptions} = usePostLanguageOptions()
return {postLanguageOptions}
},
data () { data () {
return { return {
subjectLineOptions: ['email', 'noop', 'masto'].map(mode => ({ subjectLineOptions: ['email', 'noop', 'masto'].map(mode => ({
@ -118,6 +124,12 @@ const GeneralTab = {
this.$store.dispatch('setOption', { name: 'translationLanguage', value: val }) this.$store.dispatch('setOption', { name: 'translationLanguage', value: val })
} }
}, },
postLanguage: {
get: function () { return this.$store.getters.mergedConfig.postLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'postLanguage', value: val })
}
},
...SharedComputedObject() ...SharedComputedObject()
}, },
methods: { methods: {

View file

@ -44,7 +44,6 @@
<template <template
v-if="profilesExpanded" v-if="profilesExpanded"
> >
<div <div
v-for="profile in settingsProfiles" v-for="profile in settingsProfiles"
:key="profile.id" :key="profile.id"
@ -73,15 +72,24 @@
</button> </button>
</template> </template>
</div> </div>
<button class="btn button-default" @click="refreshProfiles()"> <button
class="btn button-default"
@click="refreshProfiles()"
>
{{ $t('settings.settings_profiles_refresh') }} {{ $t('settings.settings_profiles_refresh') }}
<FAIcon icon="sync" @click="refreshProfiles()" /> <FAIcon
icon="sync"
@click="refreshProfiles()"
/>
</button> </button>
<h3>{{ $t('settings.settings_profile_creation') }}</h3> <h3>{{ $t('settings.settings_profile_creation') }}</h3>
<label for="settings-profile-new-name"> <label for="settings-profile-new-name">
{{ $t('settings.settings_profile_creation_new_name_label') }} {{ $t('settings.settings_profile_creation_new_name_label') }}
</label> </label>
<input v-model="newProfileName" id="settings-profile-new-name"> <input
id="settings-profile-new-name"
v-model="newProfileName"
>
<button <button
class="btn button-default" class="btn button-default"
@click="createSettingsProfile" @click="createSettingsProfile"
@ -146,6 +154,11 @@
{{ $t('settings.show_wider_shortcuts') }} {{ $t('settings.show_wider_shortcuts') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<BooleanSetting path="displayPageBackgrounds">
{{ $t('settings.show_page_backgrounds') }}
</BooleanSetting>
</li>
<li> <li>
<BooleanSetting path="stopGifs"> <BooleanSetting path="stopGifs">
{{ $t('settings.stop_gifs') }} {{ $t('settings.stop_gifs') }}
@ -483,14 +496,6 @@
</BooleanSetting> </BooleanSetting>
</li> </li>
</ul> </ul>
<li>
<BooleanSetting
path="useAtIcon"
expert="1"
>
{{ $t('settings.use_at_icon') }}
</BooleanSetting>
</li>
<li> <li>
<BooleanSetting path="mentionLinkShowAvatar"> <BooleanSetting path="mentionLinkShowAvatar">
{{ $t('settings.mention_link_show_avatar') }} {{ $t('settings.mention_link_show_avatar') }}
@ -588,6 +593,15 @@
{{ $t('settings.post_status_content_type') }} {{ $t('settings.post_status_content_type') }}
</ChoiceSetting> </ChoiceSetting>
</li> </li>
<li>
<ChoiceSetting
id="postLanguage"
path="postLanguage"
:options="postLanguageOptions"
>
{{ $t('settings.post_language') }}
</ChoiceSetting>
</li>
<li> <li>
<BooleanSetting <BooleanSetting
path="alwaysShowNewPostButton" path="alwaysShowNewPostButton"

View file

@ -10,17 +10,20 @@ import SelectableList from 'src/components/selectable_list/selectable_list.vue'
import ProgressButton from 'src/components/progress_button/progress_button.vue' import ProgressButton from 'src/components/progress_button/progress_button.vue'
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription' import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
import withLoadMore from 'src/components/../hocs/with_load_more/with_load_more'
const BlockList = withSubscription({ const BlockList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchBlocks'), fetch: (props, $store) => $store.dispatch('fetchBlocks'),
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []), select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
childPropName: 'items' childPropName: 'items',
destroy: () => {}
})(SelectableList) })(SelectableList)
const MuteList = withSubscription({ const MuteList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchMutes'), fetch: (props, $store) => $store.dispatch('fetchMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []), select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
childPropName: 'items' childPropName: 'items',
destroy: () => {}
})(SelectableList) })(SelectableList)
const DomainMuteList = withSubscription({ const DomainMuteList = withSubscription({
@ -82,7 +85,7 @@ const MutesAndBlocks = {
// check is it's a local user // check is it's a local user
if (user && user.is_local) { if (user && user.is_local) {
// append the instance address // append the instance address
// eslint-disable-next-line no-undef
return user.screen_name + '@' + location.hostname return user.screen_name + '@' + location.hostname
} }
return user.screen_name return user.screen_name

View file

@ -10,7 +10,7 @@
:query="queryUserIds" :query="queryUserIds"
:placeholder="$t('settings.search_user_to_block')" :placeholder="$t('settings.search_user_to_block')"
> >
<template v-slot="row"> <template #default="row">
<BlockCard <BlockCard
:user-id="row.item" :user-id="row.item"
/> />
@ -21,7 +21,7 @@
:refresh="true" :refresh="true"
:get-key="i => i" :get-key="i => i"
> >
<template v-slot:header="{selected}"> <template #header="{selected}">
<div class="bulk-actions"> <div class="bulk-actions">
<ProgressButton <ProgressButton
v-if="selected.length > 0" v-if="selected.length > 0"
@ -29,7 +29,7 @@
:click="() => blockUsers(selected)" :click="() => blockUsers(selected)"
> >
{{ $t('user_card.block') }} {{ $t('user_card.block') }}
<template v-slot:progress> <template #progress>
{{ $t('user_card.block_progress') }} {{ $t('user_card.block_progress') }}
</template> </template>
</ProgressButton> </ProgressButton>
@ -39,16 +39,16 @@
:click="() => unblockUsers(selected)" :click="() => unblockUsers(selected)"
> >
{{ $t('user_card.unblock') }} {{ $t('user_card.unblock') }}
<template v-slot:progress> <template #progress>
{{ $t('user_card.unblock_progress') }} {{ $t('user_card.unblock_progress') }}
</template> </template>
</ProgressButton> </ProgressButton>
</div> </div>
</template> </template>
<template v-slot:item="{item}"> <template #item="{item}">
<BlockCard :user-id="item" /> <BlockCard :user-id="item" />
</template> </template>
<template v-slot:empty> <template #empty>
{{ $t('settings.no_blocks') }} {{ $t('settings.no_blocks') }}
</template> </template>
</BlockList> </BlockList>
@ -63,7 +63,7 @@
:query="queryUserIds" :query="queryUserIds"
:placeholder="$t('settings.search_user_to_mute')" :placeholder="$t('settings.search_user_to_mute')"
> >
<template v-slot="row"> <template #default="row">
<MuteCard <MuteCard
:user-id="row.item" :user-id="row.item"
/> />
@ -74,7 +74,7 @@
:refresh="true" :refresh="true"
:get-key="i => i" :get-key="i => i"
> >
<template v-slot:header="{selected}"> <template #header="{selected}">
<div class="bulk-actions"> <div class="bulk-actions">
<ProgressButton <ProgressButton
v-if="selected.length > 0" v-if="selected.length > 0"
@ -82,7 +82,7 @@
:click="() => muteUsers(selected)" :click="() => muteUsers(selected)"
> >
{{ $t('user_card.mute') }} {{ $t('user_card.mute') }}
<template v-slot:progress> <template #progress>
{{ $t('user_card.mute_progress') }} {{ $t('user_card.mute_progress') }}
</template> </template>
</ProgressButton> </ProgressButton>
@ -92,16 +92,16 @@
:click="() => unmuteUsers(selected)" :click="() => unmuteUsers(selected)"
> >
{{ $t('user_card.unmute') }} {{ $t('user_card.unmute') }}
<template v-slot:progress> <template #progress>
{{ $t('user_card.unmute_progress') }} {{ $t('user_card.unmute_progress') }}
</template> </template>
</ProgressButton> </ProgressButton>
</div> </div>
</template> </template>
<template v-slot:item="{item}"> <template #item="{item}">
<MuteCard :user-id="item" /> <MuteCard :user-id="item" />
</template> </template>
<template v-slot:empty> <template #empty>
{{ $t('settings.no_mutes') }} {{ $t('settings.no_mutes') }}
</template> </template>
</MuteList> </MuteList>
@ -114,7 +114,7 @@
:query="queryKnownDomains" :query="queryKnownDomains"
:placeholder="$t('settings.type_domains_to_mute')" :placeholder="$t('settings.type_domains_to_mute')"
> >
<template v-slot="row"> <template #default="row">
<DomainMuteCard <DomainMuteCard
:domain="row.item" :domain="row.item"
/> />
@ -125,7 +125,7 @@
:refresh="true" :refresh="true"
:get-key="i => i" :get-key="i => i"
> >
<template v-slot:header="{selected}"> <template #header="{selected}">
<div class="bulk-actions"> <div class="bulk-actions">
<ProgressButton <ProgressButton
v-if="selected.length > 0" v-if="selected.length > 0"
@ -133,16 +133,16 @@
:click="() => unmuteDomains(selected)" :click="() => unmuteDomains(selected)"
> >
{{ $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>
</div> </div>
</template> </template>
<template v-slot:item="{item}"> <template #item="{item}">
<DomainMuteCard :domain="item" /> <DomainMuteCard :domain="item" />
</template> </template>
<template v-slot:empty> <template #empty>
{{ $t('settings.no_mutes') }} {{ $t('settings.no_mutes') }}
</template> </template>
</DomainMuteList> </DomainMuteList>

View file

@ -33,6 +33,7 @@ const ProfileTab = {
newName: this.$store.state.users.currentUser.name_unescaped, newName: this.$store.state.users.currentUser.name_unescaped,
newBio: unescape(this.$store.state.users.currentUser.description), newBio: unescape(this.$store.state.users.currentUser.description),
newLocked: this.$store.state.users.currentUser.locked, newLocked: this.$store.state.users.currentUser.locked,
newPermitFollowback: this.$store.state.users.currentUser.permit_followback,
newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })), newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })),
showRole: this.$store.state.users.currentUser.show_role, showRole: this.$store.state.users.currentUser.show_role,
role: this.$store.state.users.currentUser.role, role: this.$store.state.users.currentUser.role,
@ -129,14 +130,15 @@ const ProfileTab = {
note: this.newBio, note: this.newBio,
locked: this.newLocked, locked: this.newLocked,
// Backend notation. // Backend notation.
/* eslint-disable camelcase */
display_name: this.newName, display_name: this.newName,
fields_attributes: this.newFields.filter(el => el != null), fields_attributes: this.newFields.filter(el => el != null),
bot: this.bot, bot: this.bot,
show_role: this.showRole, show_role: this.showRole,
status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1, status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1,
permit_followback: this.permit_followback,
accepts_direct_messages_from: this.userAcceptsDirectMessagesFrom accepts_direct_messages_from: this.userAcceptsDirectMessagesFrom
/* eslint-enable camelcase */
} }
if (this.emailLanguage) { if (this.emailLanguage) {
@ -185,7 +187,7 @@ const ProfileTab = {
}) })
return return
} }
// eslint-disable-next-line no-undef
const reader = new FileReader() const reader = new FileReader()
reader.onload = ({ target }) => { reader.onload = ({ target }) => {
const img = target.result const img = target.result

View file

@ -110,11 +110,9 @@
max="730" max="730"
class="expire-posts-days" class="expire-posts-days"
:placeholder="$t('settings.expire_posts_input_placeholder')" :placeholder="$t('settings.expire_posts_input_placeholder')"
/> >
</p>
<p>
</p> </p>
<p />
<p> <p>
<interface-language-switcher <interface-language-switcher
:prompt-text="$t('settings.email_language')" :prompt-text="$t('settings.email_language')"
@ -259,6 +257,19 @@
<BooleanSetting path="serverSide_locked"> <BooleanSetting path="serverSide_locked">
{{ $t('settings.lock_account_description') }} {{ $t('settings.lock_account_description') }}
</BooleanSetting> </BooleanSetting>
<ul
class="setting-list suboptions"
:class="[{disabled: !serverSide_locked}]"
>
<li>
<BooleanSetting
path="serverSide_permitFollowback"
:disabled="!serverSide_locked"
>
{{ $t('settings.permit_followback_description') }}
</BooleanSetting>
</li>
</ul>
</li> </li>
<li> <li>
<BooleanSetting path="serverSide_discoverable"> <BooleanSetting path="serverSide_discoverable">

View file

@ -268,6 +268,10 @@
.side-drawer { .side-drawer {
overflow-x: hidden; overflow-x: hidden;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1); transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
@media (prefers-reduced-motion: reduce) {
transition-timing-function: unset;
transition: unset;
}
transition: 0.35s; transition: 0.35s;
transition-property: transform; transition-property: transform;
margin: 0 0 0 -100px; margin: 0 0 0 -100px;

View file

@ -20,6 +20,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { muteWordHits } from '../../services/status_parser/status_parser.js' import { muteWordHits } from '../../services/status_parser/status_parser.js'
import { unescape, uniqBy } from 'lodash' import { unescape, uniqBy } from 'lodash'
import StillImage from '../still-image/still-image.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -117,7 +118,8 @@ const Status = {
RichContent, RichContent,
MentionLink, MentionLink,
MentionsLine, MentionsLine,
QuoteButton QuoteButton,
StillImage
}, },
props: [ props: [
'statusoid', 'statusoid',

View file

@ -266,6 +266,16 @@
color: $fallback--cGreen; color: $fallback--cGreen;
color: var(--cGreen, $fallback--cGreen); color: var(--cGreen, $fallback--cGreen);
} }
.right-side {
display: flex;
align-items: center;
gap: 0.3em;
}
.repeat-tooltip {
flex-shrink: 0;
}
} }
.repeater-avatar { .repeater-avatar {

View file

@ -83,7 +83,7 @@
:user="statusoid.user" :user="statusoid.user"
/> />
<div class="right-side faint"> <div class="right-side faint">
<span <div
class="status-username repeater-name" class="status-username repeater-name"
:title="retweeter" :title="retweeter"
> >
@ -100,8 +100,12 @@
v-else v-else
:to="retweeterProfileLink" :to="retweeterProfileLink"
>{{ retweeter }}</router-link> >{{ retweeter }}</router-link>
</span> </div>
{{ ' ' }} {{ ' ' }}
<div
class="repeat-tooltip"
>
<FAIcon <FAIcon
icon="retweet" icon="retweet"
class="repeat-icon" class="repeat-icon"
@ -110,6 +114,7 @@
{{ $t('timeline.repeated') }} {{ $t('timeline.repeated') }}
</div> </div>
</div> </div>
</div>
<div <div
v-if="!deleted" v-if="!deleted"
@ -174,12 +179,12 @@
> >
@{{ status.user.screen_name_ui }} @{{ status.user.screen_name_ui }}
</router-link> </router-link>
<img <StillImage
v-if="!!(status.user && status.user.favicon)" v-if="!!(status.user && status.user.favicon)"
class="status-favicon" class="status-favicon"
:src="status.user.favicon" :src="status.user.favicon"
:title="faviconAlt(status)" :title="faviconAlt(status)"
> />
</span> </span>
</div> </div>
@ -190,7 +195,7 @@
> >
<Timeago <Timeago
:time="status.created_at" :time="status.created_at"
:with-direction="true" :with-direction="!compact"
:auto-update="60" :auto-update="60"
/> />
</router-link> </router-link>
@ -368,7 +373,7 @@
:controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject" :controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject"
@mediaplay="addMediaPlaying($event)" @mediaplay="addMediaPlaying($event)"
@mediapause="removeMediaPlaying($event)" @mediapause="removeMediaPlaying($event)"
@parseReady="setHeadTailLinks" @parse-ready="setHeadTailLinks"
/> />
</div> </div>
@ -476,8 +481,8 @@
/> />
<extra-buttons <extra-buttons
:status="status" :status="status"
@onError="showError" @on-error="showError"
@onSuccess="clearError" @on-success="clearError"
/> />
</div> </div>
</div> </div>

View file

@ -17,6 +17,9 @@
.emoji:hover { .emoji:hover {
transform: scale(1.4); transform: scale(1.4);
@media (prefers-reduced-motion: reduce) {
transition: unset;
}
transition: 0.05s; transition: 0.05s;
} }

View file

@ -54,7 +54,7 @@
:mfm="renderMisskeyMarkdown && (status.media_type === 'text/x.misskeymarkdown')" :mfm="renderMisskeyMarkdown && (status.media_type === 'text/x.misskeymarkdown')"
:greentext="mergedConfig.greentext" :greentext="mergedConfig.greentext"
:attentions="status.attentions" :attentions="status.attentions"
@parseReady="onParseReady" @parse-ready="onParseReady"
/> />
<div <div
v-if="status.translation" v-if="status.translation"
@ -70,7 +70,7 @@
:mfm="renderMisskeyMarkdown && (status.media_type === 'text/x.misskeymarkdown')" :mfm="renderMisskeyMarkdown && (status.media_type === 'text/x.misskeymarkdown')"
:greentext="mergedConfig.greentext" :greentext="mergedConfig.greentext"
:attentions="status.attentions" :attentions="status.attentions"
@parseReady="onParseReady" @parse-ready="onParseReady"
/> />
<div> <div>
<label class="label">{{ $t('status.override_translation_source_language') }}</label> <label class="label">{{ $t('status.override_translation_source_language') }}</label>
@ -89,7 +89,10 @@
</option> </option>
</Select> </Select>
{{ ' ' }} {{ ' ' }}
<button @click="translateStatus" class="btn button-default"> <button
class="btn button-default"
@click="translateStatus"
>
{{ $t('status.translate') }} {{ $t('status.translate') }}
</button> </button>
</div> </div>

View file

@ -14,7 +14,7 @@
:toggle-showing-tall="toggleShowingTall" :toggle-showing-tall="toggleShowingTall"
:toggle-expanding-subject="toggleExpandingSubject" :toggle-expanding-subject="toggleExpandingSubject"
:toggle-showing-long-subject="toggleShowingLongSubject" :toggle-showing-long-subject="toggleShowingLongSubject"
@parseReady="$emit('parseReady', $event)" @parse-ready="$emit('parseReady', $event)"
> >
<div v-if="status.poll && status.poll.options && !compact"> <div v-if="status.poll && status.poll.options && !compact">
<Poll <Poll

View file

@ -2,7 +2,7 @@
<Modal <Modal
v-if="modalActivated" v-if="modalActivated"
class="status-history-modal-view" class="status-history-modal-view"
@backdropClicked="closeModal" @backdrop-clicked="closeModal"
> >
<div class="status-history-modal-panel panel"> <div class="status-history-modal-panel panel">
<div class="panel-heading"> <div class="panel-heading">
@ -17,7 +17,7 @@
v-for="status in history" v-for="status in history"
:key="status.id" :key="status.id"
:statusoid="status" :statusoid="status"
:isPreview="true" :is-preview="true"
class="conversation-status status-fadein panel-body" class="conversation-status status-fadein panel-body"
/> />
</div> </div>

View file

@ -5,10 +5,10 @@
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
@show="enter" @show="enter"
> >
<template v-slot:trigger> <template #trigger>
<slot /> <slot />
</template> </template>
<template v-slot:content> <template #content>
<Status <Status
v-if="status" v-if="status"
:is-preview="true" :is-preview="true"

View file

@ -11,8 +11,9 @@ const StillImage = {
], ],
data () { data () {
return { return {
stopGifs: this.$store.getters.mergedConfig.stopGifs, stopGifs: this.$store.getters.mergedConfig.stopGifs || window.matchMedia('(prefers-reduced-motion: reduce)').matches,
isAnimated: false isAnimated: false,
imageTypeLabel: ''
} }
}, },
computed: { computed: {
@ -39,14 +40,24 @@ const StillImage = {
this.imageLoadError && this.imageLoadError() this.imageLoadError && this.imageLoadError()
}, },
detectAnimation (image) { detectAnimation (image) {
if (this.mimetype === 'image/gif' || this.src.endsWith('.gif')) { const mediaProxyAvailable = this.$store.state.instance.mediaProxyAvailable
if (!mediaProxyAvailable) {
// It's a bit aggressive to assume all images we can't find the mimetype of is animated, but necessary for
// people in need of reduced motion accessibility. As such, we'll consider those images animated if the user
// agent is set to prefer reduced motion. Otherwise, it'll just be used as an early exit.
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
// Since the canvas and images are not pixel-perfect matching (due to scaling),
// It makes the images jiggle on hover, which is not ideal for accessibility, methinks
this.isAnimated = true this.isAnimated = true
return return
} }
// harmless CORS errors without-- clean console with this.detectWithoutMediaProxy(image)
if (!this.$store.state.instance.mediaProxyAvailable) return } else {
// Animated JPEGs? this.detectWithMediaProxy(image)
if (!(this.src.endsWith('.webp') || this.src.endsWith('.png'))) return }
},
detectAnimationWithFetch (image) {
// Browser Cache should ensure image doesn't get loaded twice if cache exists // Browser Cache should ensure image doesn't get loaded twice if cache exists
fetch(image.src, { fetch(image.src, {
referrerPolicy: 'same-origin' referrerPolicy: 'same-origin'
@ -55,12 +66,20 @@ const StillImage = {
// We don't need to read the whole file so only call it once // We don't need to read the whole file so only call it once
data.body.getReader().read() data.body.getReader().read()
.then(reader => { .then(reader => {
if (this.src.endsWith('.webp') && this.isAnimatedWEBP(reader.value)) { // Ordered from least to most intensive
if (this.isGIF(reader.value)) {
this.isAnimated = true this.isAnimated = true
this.setLabel('GIF')
return return
} }
if (this.src.endsWith('.png') && this.isAnimatedPNG(reader.value)) { if (this.isAnimatedWEBP(reader.value)) {
this.isAnimated = true this.isAnimated = true
this.setLabel('WEBP')
return
}
if (this.isAnimatedPNG(reader.value)) {
this.isAnimated = true
this.setLabel('APNG')
} }
}) })
}) })
@ -68,6 +87,53 @@ const StillImage = {
// this.imageLoadError && this.imageLoadError() // this.imageLoadError && this.imageLoadError()
}) })
}, },
detectWithMediaProxy (image) {
this.detectAnimationWithFetch(image)
},
detectWithoutMediaProxy (image) {
// We'll just assume that gifs and webp are animated
const extension = image.src.split('.').pop().toLowerCase()
if (extension === 'gif') {
this.isAnimated = true
this.setLabel('GIF')
return
}
if (extension === 'webp') {
this.isAnimated = true
this.setLabel('WEBP')
return
}
// Beware the apng! use this if ye dare
// if (extension === 'png') {
// this.isAnimated = true
// this.setLabel('PNG')
// return
// }
// Hail mary for extensionless
if (extension.includes('/')) {
// Don't mind the CORS error barrage
this.detectAnimationWithFetch(image)
}
},
setLabel (name) {
this.imageTypeLabel = name;
},
isGIF (data) {
// I am a perfectly sane individual
//
// GIF HEADER CHUNK
// === START HEADER ===
// 47 49 46 38 ("GIF8")
const gifHeader = [0x47, 0x49, 0x46];
for (let i = 0; i < 3; i++) {
if (data[i] !== gifHeader[i]) {
return false;
}
}
return true
},
isAnimatedWEBP (data) { isAnimatedWEBP (data) {
/** /**
* WEBP HEADER CHUNK * WEBP HEADER CHUNK
@ -102,14 +168,53 @@ const StillImage = {
return (str.substring(0, idatPos > 0 ? idatPos : 0).indexOf('acTL') > 0) return (str.substring(0, idatPos > 0 ? idatPos : 0).indexOf('acTL') > 0)
}, },
drawThumbnail() { drawThumbnail() {
const canvas = this.$refs.canvas const canvas = this.$refs.canvas;
if (!this.$refs.canvas) return if (!canvas) return;
const image = this.$refs.src
const width = image.naturalWidth const context = canvas.getContext('2d');
const height = image.naturalHeight const image = this.$refs.src;
canvas.width = width const parentElement = canvas.parentElement;
canvas.height = height
canvas.getContext('2d').drawImage(image, 0, 0, width, height) // Draw the quick, unscaled version first
context.drawImage(image, 0, 0, parentElement.clientWidth, parentElement.clientHeight);
// Use requestAnimationFrame to schedule the scaling to the next frame
requestAnimationFrame(() => {
// Compute scaling ratio between the natural dimensions of the image and its display dimensions
const scalingRatioWidth = parentElement.clientWidth / image.naturalWidth;
const scalingRatioHeight = parentElement.clientHeight / image.naturalHeight;
// Adjust for high-DPI displays
const ratio = window.devicePixelRatio || 1;
canvas.width = image.naturalWidth * scalingRatioWidth * ratio;
canvas.height = image.naturalHeight * scalingRatioHeight * ratio;
canvas.style.width = `${parentElement.clientWidth}px`;
canvas.style.height = `${parentElement.clientHeight}px`;
context.scale(ratio, ratio);
// Maintain the aspect ratio of the image
const imgAspectRatio = image.naturalWidth / image.naturalHeight;
const canvasAspectRatio = parentElement.clientWidth / parentElement.clientHeight;
let drawWidth, drawHeight;
if (imgAspectRatio > canvasAspectRatio) {
drawWidth = parentElement.clientWidth;
drawHeight = parentElement.clientWidth / imgAspectRatio;
} else {
drawHeight = parentElement.clientHeight;
drawWidth = parentElement.clientHeight * imgAspectRatio;
}
context.clearRect(0, 0, canvas.width, canvas.height); // Clear the previous unscaled image
context.imageSmoothingEnabled = true;
context.imageSmoothingQuality = 'high';
// Draw the good one for realsies
const dx = (parentElement.clientWidth - drawWidth) / 2;
const dy = (parentElement.clientHeight - drawHeight) / 2;
context.drawImage(image, dx, dy, drawWidth, drawHeight);
});
} }
}, },
updated () { updated () {

View file

@ -1,9 +1,16 @@
<template> <template>
<div <div
ref="still-image"
class="still-image" class="still-image"
:class="{ animated: animated }" :class="{ animated: animated }"
:style="style" :style="style"
> >
<div
v-if="animated && imageTypeLabel"
class="image-type-label"
>
{{ imageTypeLabel }}
</div>
<canvas <canvas
v-if="animated" v-if="animated"
ref="canvas" ref="canvas"
@ -57,30 +64,26 @@
} }
} }
&.animated { .image-type-label {
&::before {
zoom: var(--_still_image-label-scale, 1);
content: 'gif';
position: absolute; position: absolute;
top: 0.25em;
left: 0.25em;
line-height: 1; line-height: 1;
font-size: 0.7em; font-size: 0.6em;
top: 0.5em;
left: 0.5em;
background: rgba(127, 127, 127, 0.5); background: rgba(127, 127, 127, 0.5);
color: #fff; color: #fff;
display: block;
padding: 2px 4px; padding: 2px 4px;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius); border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
z-index: 2; z-index: 2;
visibility: var(--_still-image-label-visibility, visible); visibility: var(--_still-image-label-visibility, visible);
} }
&.animated {
&:hover canvas { &:hover canvas {
display: none; display: none;
} }
&:hover::before { &:hover .image-type-label {
visibility: var(--_still-image-label-visibility, hidden); visibility: var(--_still-image-label-visibility, hidden);
} }

View file

@ -32,7 +32,7 @@
:dive="dive ? () => dive(status.id) : undefined" :dive="dive ? () => dive(status.id) : undefined"
@goto="setHighlight" @goto="setHighlight"
@toggleExpanded="toggleExpanded" @toggle-expanded="toggleExpanded"
/> />
<div <div
v-if="currentReplies.length && threadShowing" v-if="currentReplies.length && threadShowing"

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