Compare commits

...

168 Commits

Author SHA1 Message Date
Vivian Lim 205f31e3dc Don't stretch image placeholders 2018-09-16 14:32:31 -07:00
Vivian Lim 267cf6143e Merge branch 'snoot' of ssh://vvn.space:2222/vivlim/pleroma-fe into snoot 2018-09-16 14:26:50 -07:00
Vivian Lim bc860b74f6 Merge branch 'develop' of git.pleroma.social:pleroma/pleroma-fe into snoot 2018-09-16 14:25:09 -07:00
kaniini 7887e42fca Merge branch 'fixAutocomplete' into 'develop'
Fixes broken autocomplete for users

See merge request pleroma/pleroma-fe!344
2018-09-13 19:30:28 +00:00
Henry 9989ad29df Merge branch 'patch-1' into 'develop'
Update Hebrew translation

See merge request pleroma/pleroma-fe!345
2018-09-12 16:43:19 +00:00
Artik Banana 9b95c4c7b5 Update Hebrew translation 2018-09-12 16:16:55 +00:00
Henry c94b9796ae Merge branch 'debug-who-to-follow-panel' into 'develop'
Small debug for Who to follow panel

See merge request pleroma/pleroma-fe!342
2018-09-12 08:53:13 +00:00
Henry Jameson 89f9b3a468 fixed autocomplete 2018-09-12 11:46:02 +03:00
Henry 4bff6f12ed Merge branch 'feature/update-de-translation' into 'develop'
updated german translation

See merge request pleroma/pleroma-fe!293
2018-09-09 15:41:24 +00:00
Hakaba Hitoyo 207c188c16 Debug 2018-09-09 15:22:55 +00:00
Henry dda0effd65 Merge branch 'pizzaiolo/pleroma-fe-patch-1' into 'develop'
Update and fix messages.js (pt, eo)

See merge request pleroma/pleroma-fe!341
2018-09-09 13:27:19 +00:00
Henry Jameson aea00310c6 fix lint 2018-09-09 16:01:44 +03:00
Henry 8455936cd6 Merge branch 'develop' into 'develop'
Update the Occitan language - Fixed Linting

See merge request pleroma/pleroma-fe!339
2018-09-09 12:58:09 +00:00
kaniini ed6abeacad Merge branch 'post-polish-fixes' into 'develop'
Post polish fixes

See merge request pleroma/pleroma-fe!333
2018-09-09 11:59:49 +00:00
kaniini 369fd648f6 Merge branch 'remove-outdated-settings' into 'develop'
Remove outdated settings

See merge request pleroma/pleroma-fe!335
2018-09-09 11:56:34 +00:00
kaniini eb93034a40 Merge branch 'simplify-sensitivity-label' into 'develop'
Simplified image sensitivity label

See merge request pleroma/pleroma-fe!334
2018-09-09 11:55:49 +00:00
kaniini 3f72b611c1 Merge branch 'prime-number-step-for-who-to-follow-panel' into 'develop'
Prime number step for Who to follow panel

See merge request pleroma/pleroma-fe!340
2018-09-09 11:55:25 +00:00
Hakaba Hitoyo 3a17252929 use prime number step for Who to follow panel 2018-09-09 14:34:51 +09:00
Henry Jameson 5726be6830 fix 2018-09-08 05:16:25 +03:00
Artik Banana 78e12bc812 Revert "Revert "Update messages.js""
This reverts commit 385484566a
2018-09-07 16:51:25 +00:00
Henry 2eab0cb115 Merge branch 'feature/features-panel' into 'develop'
Features panel

See merge request pleroma/pleroma-fe!331
2018-09-07 16:37:24 +00:00
Henry 385484566a Revert "Update messages.js"
This reverts commit 5e1f0d9416
2018-09-07 16:22:59 +00:00
Artik Banana 5e1f0d9416 Update messages.js
Update the Occitan language - Fixed Linting
2018-09-07 16:18:01 +00:00
hakabahitoyo 8b94ea28ec remove formatting options 2018-09-04 14:50:02 +09:00
hakabahitoyo dbf24e1fbf Remove outdated settings 2018-09-04 11:15:00 +09:00
Hakaba Hitoyo 49ab19c342 features panel i18n 2018-09-04 10:59:02 +09:00
Hakaba Hitoyo 65115bfc7f features panes supports chat & gopher 2018-09-04 10:44:25 +09:00
Henry Jameson dc0b47e8bd fix collapse link being too small 2018-09-03 23:30:07 +03:00
Henry c348a3ec11 Merge branch 'patch-1' into 'develop'
Fix friends query

See merge request pleroma/pleroma-fe!332
2018-09-03 20:07:11 +00:00
Henry Jameson f9eb9e0b70 small fix for non-square gif avatars 2018-09-03 22:49:46 +03:00
Henry Jameson 6e64324d37 Fixed collapseMessageWithSubjectLocal always using instance-provided config. 2018-09-03 22:40:45 +03:00
Henry Jameson d4f9d21857 Fix last place with usercard having wrong width 2018-09-03 22:23:53 +03:00
Henry Jameson 08b044c365 Fixed non-masked image looking weird in chrome. 2018-09-03 22:18:59 +03:00
Henry Jameson a6047e0ad5 Kinda went back to using align-items: stretch. Fixed error message floating. 2018-09-03 22:12:18 +03:00
Henry Jameson 36101abc22 Simplified image sensitivity label 2018-09-03 18:52:07 +03:00
Dingdreher 900aaefb98 Update users.js 2018-09-03 12:01:05 +00:00
hakabahitoyo fa477dd41f show features panel only if not login 2018-09-03 16:02:06 +09:00
hakabahitoyo 38b683b30c update 2018-09-03 15:57:22 +09:00
Hakaba Hitoyo a99869146f debug 2018-09-03 15:24:49 +09:00
Hakaba Hitoyo 049e2397b1 update 2018-09-03 15:23:09 +09:00
hakabahitoyo 07ef43c7b5 debug 2018-09-03 14:55:40 +09:00
Hakaba Hitoyo 52fe01e4d8 mock features panel 2018-09-03 14:43:10 +09:00
Shpuld Shpludson 20b0ffc0b9 Merge branch 'imvomitingasicodethisshit' into 'develop'
Made showing format selection optional and default to false

See merge request pleroma/pleroma-fe!329
2018-09-01 05:57:58 +00:00
Shpuld Shpludson a8ae2a5b11 Merge branch 'fix/put-sense-into-rich-text-styles' into 'develop'
Add styles for h1/2/3/4/5 tags in status html for rich text

See merge request pleroma/pleroma-fe!328
2018-09-01 05:54:47 +00:00
Henry Jameson d6240c25cf small fix 2018-08-31 17:24:46 +03:00
Henry Jameson 17f30190e0 Made showing format selection optional and default to false 2018-08-31 17:00:41 +03:00
shpuld 77e8933b2e Fix mistakes 2018-08-31 16:13:43 +03:00
shpuld 384c80e238 Change styles a bit, make pre-formatted blocks not overflow but scroll 2018-08-31 16:04:52 +03:00
shpuld f07e6b271c Add styles for h1/2/3/4/5 tags in status html 2018-08-31 15:11:27 +03:00
kaniini 33b13d3775 Merge branch 'fixup/post-status-styling' into 'develop'
post status form: styling fixes for content-type selector, align icons with selector

See merge request pleroma/pleroma-fe!327
2018-08-31 04:24:53 +00:00
William Pitcock f9bfe2ea37 post status form: styling fixes for content-type selector, align icons with selector 2018-08-31 04:23:06 +00:00
kaniini 958acbab8d Merge branch 'polish' into 'develop'
Another one of those MR that fixes many many small-to-medium things

Closes #92, #75, #122, #52, #72, and #87

See merge request pleroma/pleroma-fe!324
2018-08-31 04:10:17 +00:00
kaniini 48391a45ba Merge branch 'bugfix/fetch-friends-id' into 'develop'
Pass user id to fetchFriends

See merge request pleroma/pleroma-fe!326
2018-08-31 04:09:33 +00:00
meireikei 2e37e8cf39 Pass user id to fetchFriends
It looks like the API that fetchFriends expects was changed, to require
an ID.
2018-08-31 04:07:25 +00:00
kaniini e71b0411aa Merge branch 'post-button-labels' into 'develop'
Add labels to post buttons

See merge request pleroma/pleroma-fe!323
2018-08-31 04:04:44 +00:00
kaniini 84aa1ecdcd Merge branch 'feature/rich-text' into 'develop'
initial rich text authoring support

See merge request pleroma/pleroma-fe!325
2018-08-31 03:54:37 +00:00
William Pitcock cabcb5c81b i18n: make "plain text" translatable 2018-08-31 03:42:12 +00:00
William Pitcock 9a43ba73e6 add the ability to select a post's content type 2018-08-31 03:42:11 +00:00
Henry Jameson 8c07e63f77 fix 2018-08-31 04:39:07 +03:00
Henry Jameson 46d8b55d44 Merge remote-tracking branch 'upstream/develop' into polish
* upstream/develop:
  i18n: make "plain text" translatable
  add the ability to select a post's content type
2018-08-31 04:20:40 +03:00
Henry Jameson e99534ef71 added option for logo in navbar to follow color scheme of the rest of the site
also fixed potential mess-up between api/static configs
2018-08-31 04:06:00 +03:00
William Pitcock 20a67e6809 i18n: make "plain text" translatable 2018-08-31 00:44:25 +00:00
William Pitcock 38e3c2493d add the ability to select a post's content type 2018-08-31 00:42:42 +00:00
Henry Jameson 63fdad8703 fix 2018-08-30 22:59:52 +03:00
Henry Jameson 1461a52ade vodka translations 2018-08-30 20:45:50 +03:00
Henry Jameson 065d1c7f49 fix 2018-08-30 20:16:36 +03:00
Henry Jameson 3b9cb1384a fix 2018-08-30 19:53:13 +03:00
Henry Jameson fa4c4c9122 fixed #87 2018-08-30 18:23:43 +03:00
Henry Jameson fa1116249d fixup! panel styling cleanup 2018-08-30 17:13:54 +03:00
Henry Jameson 42584b1a34 fixup! fixup! Added logic to process reply to favorite request and update likes counter accordingly. Should fix some of cases of doubled likes and likes counter not decrementing. 2018-08-30 16:52:38 +03:00
Henry Jameson 4589466917 fixup! Added logic to process reply to favorite request and update likes counter accordingly. Should fix some of cases of doubled likes and likes counter not decrementing. 2018-08-30 16:34:24 +03:00
Henry Jameson 507d5bc444 fixup! Added logic to process reply to favorite request and update likes counter accordingly. Should fix some of cases of doubled likes and likes counter not decrementing. 2018-08-30 16:27:35 +03:00
Henry Jameson 1246463f96 fixes broken nsfw hider in notifications 2018-08-30 16:21:10 +03:00
Henry Jameson fb7f65481e restored "progress" cursor indicator for loading nsfw images 2018-08-30 16:11:31 +03:00
Henry Jameson e58221fb87 fixed #72 2018-08-30 16:02:53 +03:00
Henry Jameson b0e0686c7f Added ability to hide certain types of notifications 2018-08-28 21:21:29 +03:00
Henry Jameson 66a22762c2 fixup! Separated tab-switcher into a reusable component. This depends on JSX addition 2018-08-28 16:22:49 +03:00
Henry Jameson da362b2b88 minor style tweaks 2018-08-28 16:20:04 +03:00
Henry Jameson 330288b4cd panel styling cleanup 2018-08-28 16:14:32 +03:00
Henry Jameson c3b27ab4c2 moved replies filtering to "filter" category in settings, made it more consistent 2018-08-28 15:47:42 +03:00
Henry Jameson cff4177bf3 settings page update 2018-08-28 15:38:07 +03:00
Henry Jameson b48a3210a3 tabs for settings 2018-08-28 14:28:05 +03:00
Henry Jameson 8e560676f1 allow multiple file upload 2018-08-28 14:05:03 +03:00
Henry Jameson 68d15f665e Show lock icon instead of hiding repeat button, tusky-style. Added hint
explaining what's going on. Fixes favorite button jumping left and right
depending on post visibility
2018-08-28 13:42:44 +03:00
Henry Jameson 226849b26e Added logic to process reply to favorite request and update likes counter
accordingly. Should fix some of cases of doubled likes and likes counter not decrementing.
2018-08-27 23:15:58 +03:00
Henry Jameson fd604dfd2a fixed still-image not preserving original aspect ratio and resolution. 2018-08-27 22:40:30 +03:00
Henry Jameson eacbd9b500 Separated tab-switcher into a reusable component. This depends on JSX addition 2018-08-27 22:22:25 +03:00
Henry Jameson b4cc1e020b added JSX support 2018-08-27 21:25:00 +03:00
Ekaterina Vaartis d2640d4bb5 Add titles to source/expand buttons 2018-08-27 12:55:46 +03:00
Ekaterina Vaartis 14c1704ea1 Add titles to post visibility icons 2018-08-27 12:55:46 +03:00
kaniini 2dd99c7dd9 Merge branch 'emoji-shortcode-startswith' into 'develop'
For user and emoji shortcode autocomplete, match using startsWith() instead of match().

Closes #135

See merge request pleroma/pleroma-fe!320
2018-08-26 20:32:00 +00:00
kaniini 257da5c740 Merge branch 'remove-unused-settings' into 'develop'
Remove unused settings

See merge request pleroma/pleroma-fe!319
2018-08-26 20:31:25 +00:00
scarlett 74a6df8a55 Match users using startsWith instead of match. 2018-08-26 13:51:21 +01:00
scarlett b68ebf3056 Match emoji using startsWith instead of match. 2018-08-26 13:50:36 +01:00
hakabahitoyo 7fc4506d29 remove-unused-settings 2018-08-26 17:32:58 +09:00
kaniini bc4f09b775 Merge branch 'serverside-frontend-configuration-2' into 'develop'
serverside-frontend-configuration-2

See merge request pleroma/pleroma-fe!312
2018-08-26 06:20:19 +00:00
kaniini 256aa25a11 Merge branch 'reply-preserve-subject' into 'develop'
Preserve subject in replies.

See merge request pleroma/pleroma-fe!318
2018-08-26 01:22:13 +00:00
scarlett 54ac0dfefd Preserve subject in replies. 2018-08-26 01:50:11 +01:00
kaniini a7c6007d54 Merge branch 'attachment-collapse' into 'develop'
When a post with a subject is collapsed, hide its attachments.

See merge request pleroma/pleroma-fe!316
2018-08-26 00:02:32 +00:00
kaniini 5bb5ef43ef Merge branch 'language-override' into 'develop'
Make interface language configurable from settings

Closes #36

See merge request pleroma/pleroma-fe!315
2018-08-26 00:01:33 +00:00
scarlett 52ce86ed57 Don't use nsfw clickthrough if the post is collapsed by default. 2018-08-26 00:21:54 +01:00
kaniini 9e111f14fd Merge branch 'nsfw-attachment-marking' into 'develop'
Add a checkbox for marking a post's attachments as NSFW

See merge request pleroma/pleroma-fe!317
2018-08-25 22:25:04 +00:00
Hakaba Hitoyo c6913e3909 correct /static/config.json decoding 2018-08-26 07:01:56 +09:00
Hakaba Hitoyo 8b9e973a55 save /api/statusnet/config.json connection 2018-08-26 07:00:23 +09:00
Hakaba Hitoyo a3cc78115c rename apiStatusnetConfigSitePleromafe to apiConfig 2018-08-26 06:56:52 +09:00
Hakaba Hitoyo a81c3b1324 fix typo 2018-08-26 06:54:03 +09:00
scarlett a7811e7bd9 Add a checkbox for marking a post's attachments as NSFW 2018-08-25 22:18:43 +01:00
scarlett d50440d802 When a post with a subject is collapsed, hide its attachments. 2018-08-25 20:33:44 +01:00
Ekaterina Vaartis c1e4bfa90f Make interface language configurable from settings
The locale can now be configured in settings and is stored in
Vuex. The changes are applied immidiately after selection. The list of
languages is taken from the messages file, which contains all the
available locales (and a new value, `interfaceLanguage`, to control
the translation of this option in the options menu)

Closes #36
2018-08-25 13:29:49 +03:00
William Pitcock 30a6b7be5b attachment: add support for rendering alt text on images 2018-08-25 00:32:10 +00:00
kaniini 673f0fca3f Merge branch 'notifications' into 'develop'
Support qvitter api notifications

Closes #129

See merge request pleroma/pleroma-fe!306
2018-08-24 23:04:36 +00:00
kaniini fe906cc3f0 Merge branch 'develop' into 'notifications'
# Conflicts:
#   src/main.js
2018-08-24 23:00:56 +00:00
kaniini 55650ff7ea Merge branch 'reply-visibility' into 'develop'
Add settings for changing the visibility of replies in the timeline.

See merge request pleroma/pleroma-fe!314
2018-08-24 21:51:54 +00:00
scarlett da96294866 Don't hide replies when inConversation. 2018-08-24 21:46:45 +01:00
scarlett 60b115320f Fix indentation 2018-08-24 20:19:22 +01:00
scarlett 50b3bd22e6 Remove old implementation of isReply. 2018-08-24 20:08:49 +01:00
scarlett 296ab54301 Add settings for changing the visibility of replies in the timeline. 2018-08-24 20:04:26 +01:00
kaniini 14db3f279d Merge branch 'feature/who-to-follow-panel-uses-suggestions-api' into 'develop'
Who to follow panel uses suggestions api

See merge request pleroma/pleroma-fe!294
2018-08-24 18:46:23 +00:00
kaniini 0429963e63 Merge branch 'easy-japanese' into 'develop'
Easy Japanese translation

See merge request pleroma/pleroma-fe!305
2018-08-24 18:45:22 +00:00
kaniini 71576947ed Merge branch 'feature/update-russian-translation' into 'develop'
Update Russian translations

See merge request pleroma/pleroma-fe!313
2018-08-24 18:43:54 +00:00
kaniini b0568ca5c3 Merge branch 'center-bios' into 'develop'
Centre-align profile bios.

See merge request pleroma/pleroma-fe!311
2018-08-24 18:42:09 +00:00
dtluna 81c04fac17 Update Russian translations 2018-08-24 21:21:14 +03:00
tsukada-ecsec dfc5f170c6 update 2018-08-24 18:46:14 +09:00
Henry Jameson 13acdc4a00 fixed error not displaying for 500 error. 2018-08-22 15:51:03 +03:00
tsukada-ecsec 0647c1bb72 debug 2018-08-22 15:15:15 +09:00
tsukada-ecsec 54166c3ad3 update settings 2018-08-22 11:47:36 +09:00
tsukada-ecsec 41256045f2 revert main.js 2018-08-22 11:38:04 +09:00
tsukada-ecsec bebd9c5ec8 revert 2018-08-22 11:35:56 +09:00
scarlett 2596f22814 Centre-align profile bios. 2018-08-21 19:16:03 +01:00
Henry Jameson a196c3551a Revert "Drop the entire thing about hidden "own" timeline since it doesn't necessarily"
This reverts commit 612aa56c8b.
2018-08-21 00:21:35 +03:00
Henry Jameson b97db4912d error display 2018-08-20 20:45:54 +03:00
Henry Jameson f9b0a95969 removed style for rounding bottom part of notifications because there's now
always "load more" footer
2018-08-20 20:08:21 +03:00
Henry Jameson 3ccea3442e fix custom emoji in username, fix gif avatar not being animated when hovering on
the notification
2018-08-20 20:05:12 +03:00
Henry Jameson 35b912bce4 Merge remote-tracking branch 'upstream/develop' into notifications
* upstream/develop: (23 commits)
  Rename expandCW to collapseMessageWithSubject.
  fix indent
  Add support for configurable CW clickthrough.
  Merge upstream
  fix lint issues
  allow default visibility scope to be configured
  Revert "storing entire config instead of each separate thing of it, so that future"
  fixes hella ton of annoyances with file upload display
  using custom ascend value as suggested here: https://github.com/fontello/fontello/issues/513#issuecomment-237551101 helped.
  disable hinting because it breaks alignment on some icons (namely - locks)
  fix for timeago being ass when post has replies. added hover colors for clickable icons on the right side. Reverted line-height to its original value
  Configurable video looping, option to not to loop silent videos. Updated localization strings.
  added pointer cursor for nsfw placeholder. fixed nsfw videos requiring double-click
  Made pausing TL updating configurable. Added styles for disabled checkboxes. Shuffled settings a bit b/c all the settings are in "Attachments" section depsite the fact not all of them are attachments-related.
  storing entire config instead of each separate thing of it, so that future options won't be lost during reloads because developer forgot to update that list of settings to be persisted
  fix potential stretched spurdo
  fixed custom emoji in nickname. changed icons on right side to be more streamlined. adjusted CSS so that all text in header of post is on same baseline and all icons/images are middle-aligned.
  Add validation of the imported theme and the corresponding warning message
  Unify button styles and use min-width
  Add German localization for theme import/export
  ...
2018-08-20 20:04:54 +03:00
Henry Jameson 9e78c64d5e Hide initial desktop notifications spam when FE is opened and there's a lot of
unseen notifications.
2018-08-20 19:58:49 +03:00
Henry Jameson fa66385c5b Updated localization files 2018-08-20 19:06:04 +03:00
Henry Jameson 612aa56c8b Drop the entire thing about hidden "own" timeline since it doesn't necessarily
contain all of the users posts (it doesn't contain DMs) even though it's "us".
Since this is a workaround anyway just fetch home timeline instead. It could end
up making more queries if user doesn't post that often.
2018-08-20 19:01:54 +03:00
Henry Jameson 0b6f9c62a1 fix 2018-08-18 13:41:23 +03:00
Henry Jameson 23a1000298 fix post search query to have id +1 because search is exclusive 2018-08-16 18:13:31 +03:00
Henry Jameson cc473df314 changed the only surviving and important test to accommodate for new notifications flow. 2018-08-16 14:46:05 +03:00
Henry Jameson 3afe65352b removed notification-relevant test because the functionality they are testing do
not exist anymore. Gotta write more tho...
2018-08-16 14:07:06 +03:00
Henry Jameson 6454837ea4 Merge remote-tracking branch 'upstream/develop' into notifications
* upstream/develop: (26 commits)
  Update status.vue
  Update retweet_button.js
  Update retweet_button.vue
  Use serverside html rendering in usernames and bios if available.
  Update status.vue
  Revert "Merge branch 'feature/hide-all-status-actions-if-not-logged-in' into 'develop'"
  Hide all status actions if not logged in
  hopefully, fix linter
  Fixes broken custom emoji in autocomplete when proxying to remote BE
  Made it so that unfocused tab doesn't autostream posts when scrolled to the top
  Remove trailing whitespace
  Textarea is now focused when replying
  the missing piece for invites system
  Fixes selects having unreadable text on some browsers/OSes. Added bonus: theme switcher select now has styled options that show preview of what theme's bg/fg colors are
  fixed lint
  cleanup, fixed self-highlighting in notifications, fixed incorrect hex code handling
  added ability to pick the style of highlighting
  post-rebase fix, backported d7d787b84c
  notifs fix
  maybe i should actually add myself to contributors list?
  ...
2018-08-16 13:59:01 +03:00
Henry Jameson decc209fdc fix lint 2018-08-16 13:57:16 +03:00
Henry Jameson 693eb4b717 cleanup, updated broken favorites look + localization strings 2018-08-16 13:41:45 +03:00
Henry Jameson e8f7491003 fixed favoriting from notification column 2018-08-16 13:20:29 +03:00
Henry Jameson ef04a78634 added workaround for broken favorites 2018-08-16 13:12:31 +03:00
Sebastian Huebner 45478d4bed
i18n/messages.js: changed Folgende back to Follower 2018-08-15 10:29:19 +02:00
Sebastian Huebner 2b61be5271 Merge branch 'feature/update-de-translation' of git.pleroma.social:pleroma/pleroma-fe into feature/update-de-translation 2018-08-15 10:28:39 +02:00
Sebastian Huebner 1dddce5624 updated german translation 2018-08-15 10:18:16 +02:00
Henry Jameson ef515056b5 missing files and a plug for bad favs 2018-08-13 13:17:10 +03:00
Henry Jameson d085cc8584 undo test condition 2018-08-12 14:15:09 +03:00
Henry Jameson 63650aec29 Added support for qvitter api fetching of notifications 2018-08-12 14:14:34 +03:00
Hakaba Hitoyo d1b3d7e90f debug 2018-08-11 14:54:30 +09:00
Hakaba Hitoyo e2dae87772 update 2018-08-11 14:50:40 +09:00
Hakaba Hitoyo b7d1bb39e0 update 2018-08-11 14:35:04 +09:00
Hakaba Hitoyo 23cfec4332 Update messages.js 2018-08-10 07:04:25 +00:00
Hakaba Hitoyo 6cc1083287 フォロワーはインポートできない。 2018-08-09 07:30:22 +00:00
Hakaba Hitoyo f8834ba1fb Update messages.js 2018-08-09 07:20:35 +00:00
Hakaba Hitoyo 3398303c9b Update messages.js 2018-08-09 06:59:23 +00:00
hakabahitoyo 5e47c59615 debug 2018-08-02 19:16:48 +09:00
Hakaba Hitoyo 19e310fc67 lint 2018-08-02 18:38:43 +09:00
Hakaba Hitoyo 5900bccff3 debug 2018-08-02 18:34:12 +09:00
Hakaba Hitoyo bcd499c372 who to follow panel uses /api/v1/suggestions 2018-08-02 17:57:00 +09:00
Sebastian Huebner 687d80ed79
updated german translation 2018-07-30 11:45:23 +02:00
lambda af47d51cd1 Merge branch 'develop' into 'patch-1'
# Conflicts:
#   src/i18n/messages.js
2018-06-08 13:30:49 +00:00
pizzaiolo 9b86fc4dcd cleaning up some translations that broke the building 2018-05-17 13:24:48 +00:00
pizzaiolo c8cdfda01c fix trailing comma 2018-05-16 10:56:46 +00:00
pizzaiolo 7187a8f2dc Update and fix messages.js (pt, eo) 2018-05-06 21:25:32 +00:00
53 changed files with 1836 additions and 881 deletions

View File

@ -1,5 +1,5 @@
{
"presets": ["es2015", "stage-2"],
"plugins": ["transform-runtime", "lodash"],
"presets": ["es2015", "stage-2", "env"],
"plugins": ["transform-runtime", "lodash", "transform-vue-jsx"],
"comments": false
}

View File

@ -54,7 +54,7 @@ module.exports = {
loader: 'vue'
},
{
test: /\.js$/,
test: /\.jsx?$/,
loader: 'babel',
include: projectRoot,
exclude: /node_modules\/(?!tributejs)/

View File

@ -37,8 +37,12 @@
"autoprefixer": "^6.4.0",
"babel-core": "^6.0.0",
"babel-eslint": "^7.0.0",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^6.0.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.0.0",
"babel-plugin-transform-vue-jsx": "3",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.0.0",
"babel-preset-stage-2": "^6.0.0",
"babel-register": "^6.0.0",
@ -63,6 +67,7 @@
"html-webpack-plugin": "^2.8.1",
"http-proxy-middleware": "^0.17.2",
"inject-loader": "^2.0.1",
"iso-639-1": "^2.0.3",
"isparta-loader": "^2.0.0",
"json-loader": "^0.5.4",
"karma": "^1.3.0",

View File

@ -2,8 +2,9 @@ import UserPanel from './components/user_panel/user_panel.vue'
import NavPanel from './components/nav_panel/nav_panel.vue'
import Notifications from './components/notifications/notifications.vue'
import UserFinder from './components/user_finder/user_finder.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import ChatPanel from './components/chat_panel/chat_panel.vue'
export default {
@ -13,23 +14,55 @@ export default {
NavPanel,
Notifications,
UserFinder,
WhoToFollowPanel,
InstanceSpecificPanel,
FeaturesPanel,
WhoToFollowPanel,
ChatPanel
},
data: () => ({
mobileActivePanel: 'timeline'
mobileActivePanel: 'timeline',
supportsMask: window.CSS && window.CSS.supports && (
window.CSS.supports('mask-size', 'contain') ||
window.CSS.supports('-webkit-mask-size', 'contain') ||
window.CSS.supports('-moz-mask-size', 'contain') ||
window.CSS.supports('-ms-mask-size', 'contain') ||
window.CSS.supports('-o-mask-size', 'contain')
)
}),
created () {
// Load the locale from the storage
this.$i18n.locale = this.$store.state.config.interfaceLanguage
},
computed: {
currentUser () { return this.$store.state.users.currentUser },
background () {
return this.currentUser.background_image || this.$store.state.config.background
},
logoStyle () { return { 'background-image': `url(${this.$store.state.config.logo})` } },
enableMask () { return this.supportsMask && this.$store.state.config.logoMask },
logoStyle () {
return {
'visibility': this.enableMask ? 'hidden' : 'visible'
}
},
logoMaskStyle () {
return this.enableMask ? {
'mask-image': `url(${this.$store.state.config.logo})`
} : {
'background-color': this.enableMask ? '' : 'transparent'
}
},
logoBgStyle () {
return Object.assign({
'margin': `${this.$store.state.config.logoMargin} 0`
}, this.enableMask ? {} : {
'background-color': this.enableMask ? '' : 'transparent'
})
},
logo () { return this.$store.state.config.logo },
style () { return { 'background-image': `url(${this.background})` } },
sitename () { return this.$store.state.config.name },
chat () { return this.$store.state.chat.channel.state === 'joined' },
showWhoToFollowPanel () { return this.$store.state.config.showWhoToFollowPanel },
suggestionsEnabled () { return this.$store.state.config.suggestionsEnabled },
showInstanceSpecificPanel () { return this.$store.state.config.showInstanceSpecificPanel }
},
methods: {

View File

@ -48,7 +48,7 @@ a {
color: var(--link, $fallback--link);
}
button{
button {
user-select: none;
color: $fallback--fg;
color: var(--fg, $fallback--fg);
@ -64,10 +64,19 @@ button{
font-size: 14px;
font-family: sans-serif;
&::-moz-focus-inner {
border: none;
}
&:hover {
box-shadow: 0px 0px 4px rgba(255, 255, 255, 0.3);
}
&:active {
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
border-top: 1px solid rgba(0, 0, 0, 0.2);
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
@ -105,6 +114,7 @@ input, textarea, .select {
position: relative;
height: 29px;
line-height: 16px;
hyphens: none;
.icon-down-open {
position: absolute;
@ -226,6 +236,40 @@ nav {
position: fixed;
height: 50px;
.logo {
display: flex;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
align-items: stretch;
justify-content: center;
flex: 0 0 auto;
z-index: -1;
.mask {
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
background-color: $fallback--fg;
background-color: var(--fg, $fallback--fg);
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
img {
height: 100%;
object-fit: contain;
display: block;
flex: 0;
}
}
.inner-nav {
padding-left: 20px;
padding-right: 20px;
@ -234,9 +278,6 @@ nav {
flex-basis: 970px;
margin: auto;
height: 50px;
background-repeat: no-repeat;
background-position: center;
background-size: auto 80%;
a i {
color: $fallback--link;
@ -282,15 +323,42 @@ main-router {
}
.panel-heading {
display: flex;
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
background-size: cover;
padding: 0.6em 1.0em;
padding: .6em .6em;
text-align: left;
font-size: 1.3em;
line-height: 24px;
line-height: 28px;
background-color: $fallback--btn;
background-color: var(--btn, $fallback--btn);
align-items: baseline;
.title {
flex: 1 0 auto;
font-size: 1.3em;
}
.alert {
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
}
button {
flex-shrink: 0;
}
button, .alert {
// height: 100%;
line-height: 21px;
min-height: 0;
box-sizing: border-box;
margin: 0;
margin-left: .25em;
min-width: 1px;
align-self: stretch;
}
}
.panel-heading.stub {
@ -451,6 +519,14 @@ nav {
color: $fallback--lightFg;
color: var(--lightFg, $fallback--lightFg);
}
.text-format {
float: right;
}
div {
padding-top: 5px;
}
}
.visibility-notice {
@ -460,4 +536,3 @@ nav {
border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius);
}

View File

@ -1,7 +1,11 @@
<template>
<div id="app" v-bind:style="style">
<nav class='container' @click="scrollToTop()" id="nav">
<div class='inner-nav' :style="logoStyle">
<div class='logo' :style='logoBgStyle'>
<div class='mask' :style='logoMaskStyle'></div>
<img :src='logo' :style='logoStyle'>
</div>
<div class='inner-nav'>
<div class='item'>
<router-link :to="{ name: 'root'}">{{sitename}}</router-link>
</div>
@ -24,7 +28,8 @@
<user-panel></user-panel>
<nav-panel></nav-panel>
<instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
<who-to-follow-panel v-if="currentUser && showWhoToFollowPanel"></who-to-follow-panel>
<features-panel v-if="!currentUser"></features-panel>
<who-to-follow-panel v-if="currentUser && suggestionsEnabled"></who-to-follow-panel>
<notifications v-if="currentUser"></notifications>
</div>
</div>

View File

@ -16,7 +16,7 @@ const Attachment = {
loopVideo: this.$store.state.config.loopVideo,
showHidden: false,
loading: false,
img: this.type === 'image' && document.createElement('img')
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img')
}
},
components: {

View File

@ -3,14 +3,14 @@
<a class="placeholder" v-if="type !== 'html'" target="_blank" :href="attachment.url">[{{nsfw ? "NSFW/" : ""}}{{type.toUpperCase()}}]</a>
</div>
<div v-else class="attachment" :class="{[type]: true, loading, 'small-attachment': isSmall, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}" v-show="!isEmpty">
<a class="image-attachment" v-if="hidden" @click.prevent="toggleHidden()">
<a class="image-attachment-placeholder" v-if="hidden" @click.prevent="toggleHidden()">
<img :key="nsfwImage" :src="nsfwImage"/>
</a>
<div class="hider" v-if="nsfw && hideNsfwLocal && !hidden">
<a href="#" @click.prevent="toggleHidden()">Hide</a>
</div>
<a v-if="type === 'image' && !hidden" class="image-attachment" :href="attachment.url" target="_blank">
<a v-if="type === 'image' && !hidden" class="image-attachment" :href="attachment.url" target="_blank" :title="attachment.description">
<StillImage :class="{'small': isSmall}" referrerpolicy="no-referrer" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
</a>
@ -51,6 +51,10 @@
.nsfw-placeholder {
cursor: pointer;
&.loading {
cursor: progress;
}
}
.small-attachment {
@ -61,6 +65,7 @@
}
.attachment {
position: relative;
flex: 1 0 30%;
margin: 0.5em 0.7em 0.6em 0.0em;
align-self: flex-start;
@ -88,10 +93,6 @@
display: flex;
}
&.loading {
cursor: progress;
}
.hider {
position: absolute;
margin: 10px;
@ -179,5 +180,9 @@
image-orientation: from-image;
}
}
.image-attachment-placeholder {
// currently, no style is applied to image placeholders.
}
}
</style>

View File

@ -1,9 +1,9 @@
<template>
<div class="timeline panel panel-default">
<div class="panel-heading conversation-heading">
{{ $t('timeline.conversation') }}
<span v-if="collapsable" style="float:right;">
<small><a href="#" @click.prevent="$emit('toggleExpanded')">{{ $t('timeline.collapse') }}</a></small>
<span class="title"> {{ $t('timeline.conversation') }} </span>
<span v-if="collapsable">
<a href="#" @click.prevent="$emit('toggleExpanded')">{{ $t('timeline.collapse') }}</a>
</span>
</div>
<div class="panel-body">

View File

@ -0,0 +1,14 @@
const FeaturesPanel = {
computed: {
chat: function () {
return this.$store.state.config.chatAvailable && (!this.$store.state.chatDisabled)
},
gopher: function () { return this.$store.state.config.gopherAvailable },
whoToFollow: function () { return this.$store.state.config.suggestionsEnabled },
mediaProxy: function () { return this.$store.state.config.mediaProxyAvailable },
scopeOptions: function () { return this.$store.state.config.scopeOptionsEnabled },
textlimit: function () { return this.$store.state.config.textlimit }
}
}
export default FeaturesPanel

View File

@ -0,0 +1,29 @@
<template>
<div class="features-panel">
<div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background base04">
<div class="title">
{{$t('features_panel.title')}}
</div>
</div>
<div class="panel-body features-panel">
<ul>
<li v-if="chat">{{$t('features_panel.chat')}}</li>
<li v-if="gopher">{{$t('features_panel.gopher')}}</li>
<li v-if="whoToFollow">{{$t('features_panel.who_to_follow')}}</li>
<li v-if="mediaProxy">{{$t('features_panel.media_proxy')}}</li>
<li v-if="scopeOptions">{{$t('features_panel.scope_options')}}</li>
<li>{{$t('features_panel.text_limit')}} = {{textlimit}}</li>
</ul>
</div>
</div>
</div>
</template>
<script src="./features_panel.js" ></script>
<style lang="scss">
.features-panel li {
line-height: 24px;
}
</style>

View File

@ -0,0 +1,38 @@
<template>
<div>
<label for="interface-language-switcher" class='select'>
<select id="interface-language-switcher" v-model="language">
<option v-for="(langCode, i) in languageCodes" :value="langCode">
{{ languageNames[i] }}
</option>
</select>
<i class="icon-down-open"/>
</label>
</div>
</template>
<script>
import languagesObject from '../../i18n/messages'
import ISO6391 from 'iso-639-1'
import _ from 'lodash'
export default {
computed: {
languageCodes () {
return Object.keys(languagesObject)
},
languageNames () {
return _.map(this.languageCodes, ISO6391.getName)
},
language: {
get: function () { return this.$store.state.config.interfaceLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
this.$i18n.locale = val
}
}
}
}
</script>

View File

@ -6,8 +6,10 @@ const mediaUpload = {
const input = this.$el.querySelector('input')
input.addEventListener('change', ({target}) => {
const file = target.files[0]
this.uploadFile(file)
for (var i = 0; i < target.files.length; i++) {
let file = target.files[i]
this.uploadFile(file)
}
})
},
data () {

View File

@ -3,7 +3,7 @@
<label class="btn btn-default">
<i class="icon-spin4 animate-spin" v-if="uploading"></i>
<i class="icon-upload" v-if="!uploading"></i>
<input type=file style="position: fixed; top: -100em"></input>
<input type="file" style="position: fixed; top: -100em" multiple="true"></input>
</label>
</div>
</template>

View File

@ -12,7 +12,7 @@
<div class="name-and-action">
<span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
<span class="username" v-else :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
<span v-if="notification.type === 'favorite'">
<span v-if="notification.type === 'like'">
<i class="fa icon-star lit"></i>
<small>{{$t('notifications.favorited_you')}}</small>
</span>
@ -25,12 +25,17 @@
<small>{{$t('notifications.followed_you')}}</small>
</span>
</div>
<small class="timeago"><router-link :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
<small class="timeago"><router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
</span>
<div class="follow-text" v-if="notification.type === 'follow'">
<router-link :to="{ name: 'user-profile', params: { id: notification.action.user.id } }">@{{notification.action.user.screen_name}}</router-link>
</div>
<status v-else class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
<template v-else>
<status v-if="notification.status" class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
<div class="broken-favorite" v-else>
{{$t('notifications.broken_favorite')}}
</div>
</template>
</div>
</div>
</template>

View File

@ -1,25 +1,38 @@
import Notification from '../notification/notification.vue'
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
import { sortBy, take, filter } from 'lodash'
import { sortBy, filter } from 'lodash'
const Notifications = {
data () {
return {
visibleNotificationCount: 20
}
created () {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
notificationsFetcher.startFetching({ store, credentials })
},
computed: {
visibleTypes () {
return [
this.$store.state.config.notificationVisibility.likes && 'like',
this.$store.state.config.notificationVisibility.mentions && 'mention',
this.$store.state.config.notificationVisibility.repeats && 'repeat',
this.$store.state.config.notificationVisibility.follows && 'follow'
].filter(_ => _)
},
notifications () {
return this.$store.state.statuses.notifications
return this.$store.state.statuses.notifications.data
},
error () {
return this.$store.state.statuses.notifications.error
},
unseenNotifications () {
return filter(this.notifications, ({seen}) => !seen)
return filter(this.visibleNotifications, ({seen}) => !seen)
},
visibleNotifications () {
// Don't know why, but sortBy([seen, -action.id]) doesn't work.
let sortedNotifications = sortBy(this.notifications, ({action}) => -action.id)
sortedNotifications = sortBy(sortedNotifications, 'seen')
return take(sortedNotifications, this.visibleNotificationCount)
return sortedNotifications.filter((notification) => this.visibleTypes.includes(notification.type))
},
unseenCount () {
return this.unseenNotifications.length
@ -40,6 +53,15 @@ const Notifications = {
methods: {
markAsSeen () {
this.$store.commit('markNotificationsAsSeen', this.visibleNotifications)
},
fetchOlderNotifications () {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
notificationsFetcher.fetchAndUpdate({
store,
credentials,
older: true
})
}
}
}

View File

@ -4,44 +4,26 @@
// a bit of a hack to allow scrolling below notifications
padding-bottom: 15em;
.panel {
background: $fallback--bg;
background: var(--bg, $fallback--bg)
}
.panel-body {
border-color: $fallback--border;
border-color: var(--border, $fallback--border)
}
.panel-heading {
// force the text to stay centered, while keeping
// the button in the right side of the panel heading
position: relative;
background: $fallback--btn;
background: var(--btn, $fallback--btn);
color: $fallback--fg;
color: var(--fg, $fallback--fg);
.read-button {
position: absolute;
right: 0.7em;
height: 1.8em;
line-height: 100%;
}
}
.unseen-count {
display: inline-block;
background-color: $fallback--cRed;
background-color: var(--cRed, $fallback--cRed);
text-shadow: 0px 0px 3px rgba(0, 0, 0, 0.5);
min-width: 1.3em;
border-radius: 1.3em;
margin: 0 0.2em 0 -0.4em;
border-radius: 99px;
min-width: 22px;
max-width: 22px;
min-height: 22px;
max-height: 22px;
color: white;
font-size: 0.9em;
font-size: 15px;
line-height: 22px;
text-align: center;
line-height: 1.3em;
vertical-align: middle
}
.loadmore-error {
color: $fallback--fg;
color: var(--fg, $fallback--fg);
}
.unseen {
@ -54,7 +36,18 @@
box-sizing: border-box;
display: flex;
border-bottom: 1px solid;
border-bottom-color: inherit;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
.broken-favorite {
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
color: $fallback--faint;
color: var(--faint, $fallback--faint);
background-color: $fallback--cAlertRed;
background-color: var(--cAlertRed, $fallback--cAlertRed);
padding: 2px .5em
}
.avatar-compact {
width: 32px;
@ -69,7 +62,7 @@
}
}
&:hover .animated.avatar {
&:hover .animated.avatar-compact {
canvas {
display: none;
}
@ -145,6 +138,13 @@
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
img {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
}
.timeago {
float: right;
@ -194,15 +194,4 @@
margin-bottom: 0.3em;
}
}
// ugly as heck
&:last-child {
border-bottom: none;
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
.status-el {
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
}
}
}

View File

@ -2,8 +2,13 @@
<div class="notifications">
<div class="panel panel-default">
<div class="panel-heading">
<span class="unseen-count" v-if="unseenCount">{{unseenCount}}</span>
{{$t('notifications.notifications')}}
<div class="title">
{{$t('notifications.notifications')}}
<span class="unseen-count" v-if="unseenCount">{{unseenCount}}</span>
</div>
<div @click.prevent class="loadmore-error alert error" v-if="error">
{{$t('timeline.error_fetching')}}
</div>
<button v-if="unseenCount" @click.prevent="markAsSeen" class="read-button">{{$t('notifications.read')}}</button>
</div>
<div class="panel-body">
@ -11,6 +16,12 @@
<notification :notification="notification"></notification>
</div>
</div>
<div class="panel-footer">
<a href="#" v-on:click.prevent='fetchOlderNotifications()' v-if="!notifications.loading">
<div class="new-status-notification text-center panel-footer">{{$t('notifications.load_older')}}</div>
</a>
<div class="new-status-notification text-center panel-footer" v-else>...</div>
</div>
</div>
</div>
</template>

View File

@ -24,7 +24,8 @@ const PostStatusForm = {
'replyTo',
'repliedUser',
'attentions',
'messageScope'
'messageScope',
'subject'
],
components: {
MediaUpload
@ -52,7 +53,10 @@ const PostStatusForm = {
posting: false,
highlighted: 0,
newStatus: {
spoilerText: this.subject,
status: statusText,
contentType: 'text/plain',
nsfw: false,
files: [],
visibility: this.messageScope || this.$store.state.users.currentUser.default_scope
},
@ -71,8 +75,11 @@ const PostStatusForm = {
candidates () {
const firstchar = this.textAtCaret.charAt(0)
if (firstchar === '@') {
const matchedUsers = filter(this.users, (user) => (String(user.name + user.screen_name)).toUpperCase()
.match(this.textAtCaret.slice(1).toUpperCase()))
const query = this.textAtCaret.slice(1).toUpperCase()
const matchedUsers = filter(this.users, (user) => {
return user.screen_name.toUpperCase().startsWith(query) ||
user.name && user.name.toUpperCase().startsWith(query)
})
if (matchedUsers.length <= 0) {
return false
}
@ -86,7 +93,7 @@ const PostStatusForm = {
}))
} else if (firstchar === ':') {
if (this.textAtCaret === ':') { return }
const matchedEmoji = filter(this.emoji.concat(this.customEmoji), (emoji) => emoji.shortcode.match(this.textAtCaret.slice(1)))
const matchedEmoji = filter(this.emoji.concat(this.customEmoji), (emoji) => emoji.shortcode.startsWith(this.textAtCaret.slice(1)))
if (matchedEmoji.length <= 0) {
return false
}
@ -135,6 +142,9 @@ const PostStatusForm = {
},
scopeOptionsEnabled () {
return this.$store.state.config.scopeOptionsEnabled
},
formattingOptionsEnabled () {
return this.$store.state.config.formattingOptionsEnabled
}
},
methods: {
@ -204,15 +214,18 @@ const PostStatusForm = {
status: newStatus.status,
spoilerText: newStatus.spoilerText || null,
visibility: newStatus.visibility,
sensitive: newStatus.nsfw,
media: newStatus.files,
store: this.$store,
inReplyToStatusId: this.replyTo
inReplyToStatusId: this.replyTo,
contentType: newStatus.contentType
}).then((data) => {
if (!data.error) {
this.newStatus = {
status: '',
files: [],
visibility: newStatus.visibility
visibility: newStatus.visibility,
contentType: newStatus.contentType
}
this.$emit('posted')
let el = this.$el.querySelector('textarea')

View File

@ -32,11 +32,24 @@
@input="resize"
@paste="paste">
</textarea>
<div v-if="scopeOptionsEnabled" class="visibility-tray">
<i v-on:click="changeVis('direct')" class="icon-mail-alt" :class="vis.direct" :title="$t('post_status.scope.direct')"></i>
<i v-on:click="changeVis('private')" class="icon-lock" :class="vis.private" :title="$t('post_status.scope.private')"></i>
<i v-on:click="changeVis('unlisted')" class="icon-lock-open-alt" :class="vis.unlisted" :title="$t('post_status.scope.unlisted')"></i>
<i v-on:click="changeVis('public')" class="icon-globe" :class="vis.public" :title="$t('post_status.scope.public')"></i>
<div class="visibility-tray">
<span class="text-format" v-if="formattingOptionsEnabled">
<label for="post-content-type" class="select">
<select id="post-content-type" v-model="newStatus.contentType" class="form-control">
<option value="text/plain">{{$t('post_status.content_type.plain_text')}}</option>
<option value="text/html">HTML</option>
<option value="text/markdown">Markdown</option>
</select>
<i class="icon-down-open"></i>
</label>
</span>
<div v-if="scopeOptionsEnabled">
<i v-on:click="changeVis('direct')" class="icon-mail-alt" :class="vis.direct" :title="$t('post_status.scope.direct')"></i>
<i v-on:click="changeVis('private')" class="icon-lock" :class="vis.private" :title="$t('post_status.scope.private')"></i>
<i v-on:click="changeVis('unlisted')" class="icon-lock-open-alt" :class="vis.unlisted" :title="$t('post_status.scope.unlisted')"></i>
<i v-on:click="changeVis('public')" class="icon-globe" :class="vis.public" :title="$t('post_status.scope.public')"></i>
</div>
</div>
</div>
<div style="position:relative;" v-if="candidates">
@ -75,6 +88,10 @@
</div>
</div>
</div>
<div class="upload_settings" v-if="newStatus.files.length > 0">
<input type="checkbox" id="filesSensitive" v-model="newStatus.nsfw">
<label for="filesSensitive">{{$t('post_status.attachments_sensitive')}}</label>
</div>
</form>
</div>
</template>

View File

@ -1,7 +1,12 @@
<template>
<div v-if="loggedIn && visibility !== 'private' && visibility !== 'direct'">
<i :class='classes' class='icon-retweet rt-active' v-on:click.prevent='retweet()'></i>
<span v-if='status.repeat_num > 0'>{{status.repeat_num}}</span>
<div v-if="loggedIn">
<template v-if="visibility !== 'private' && visibility !== 'direct'">
<i :class='classes' class='icon-retweet rt-active' v-on:click.prevent='retweet()'></i>
<span v-if='status.repeat_num > 0'>{{status.repeat_num}}</span>
</template>
<template v-else>
<i :class='classes' class='icon-lock' :title="$t('timeline.no_retweet_hint')"></i>
</template>
</div>
<div v-else-if="!loggedIn">
<i :class='classes' class='icon-retweet'></i>

View File

@ -1,22 +1,30 @@
/* eslint-env browser */
import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
import StyleSwitcher from '../style_switcher/style_switcher.vue'
import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
import { filter, trim } from 'lodash'
const settings = {
data () {
const config = this.$store.state.config
return {
hideAttachmentsLocal: this.$store.state.config.hideAttachments,
hideAttachmentsInConvLocal: this.$store.state.config.hideAttachmentsInConv,
hideNsfwLocal: this.$store.state.config.hideNsfw,
loopVideoLocal: this.$store.state.config.loopVideo,
loopVideoSilentOnlyLocal: this.$store.state.config.loopVideoSilentOnly,
muteWordsString: this.$store.state.config.muteWords.join('\n'),
autoLoadLocal: this.$store.state.config.autoLoad,
streamingLocal: this.$store.state.config.streaming,
pauseOnUnfocusedLocal: this.$store.state.config.pauseOnUnfocused,
hoverPreviewLocal: this.$store.state.config.hoverPreview,
collapseMessageWithSubjectLocal: this.$store.state.config.collapseMessageWithSubject,
stopGifs: this.$store.state.config.stopGifs,
hideAttachmentsLocal: config.hideAttachments,
hideAttachmentsInConvLocal: config.hideAttachmentsInConv,
hideNsfwLocal: config.hideNsfw,
notificationVisibilityLocal: config.notificationVisibility,
replyVisibilityLocal: config.replyVisibility,
loopVideoLocal: config.loopVideo,
loopVideoSilentOnlyLocal: config.loopVideoSilentOnly,
muteWordsString: config.muteWords.join('\n'),
autoLoadLocal: config.autoLoad,
streamingLocal: config.streaming,
pauseOnUnfocusedLocal: config.pauseOnUnfocused,
hoverPreviewLocal: config.hoverPreview,
collapseMessageWithSubjectLocal: typeof config.collapseMessageWithSubject === 'undefined'
? config.defaultCollapseMessageWithSubject
: config.collapseMessageWithSubject,
stopGifs: config.stopGifs,
loopSilentAvailable:
// Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
@ -27,7 +35,9 @@ const settings = {
}
},
components: {
StyleSwitcher
TabSwitcher,
StyleSwitcher,
InterfaceLanguageSwitcher
},
computed: {
user () {
@ -44,6 +54,21 @@ const settings = {
hideNsfwLocal (value) {
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
},
'notificationVisibilityLocal.likes' (value) {
this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
},
'notificationVisibilityLocal.follows' (value) {
this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
},
'notificationVisibilityLocal.repeats' (value) {
this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
},
'notificationVisibilityLocal.mentions' (value) {
this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
},
replyVisibilityLocal (value) {
this.$store.dispatch('setOption', { name: 'replyVisibility', value })
},
loopVideoLocal (value) {
this.$store.dispatch('setOption', { name: 'loopVideo', value })
},

View File

@ -4,76 +4,132 @@
{{$t('settings.settings')}}
</div>
<div class="panel-body">
<div class="setting-item">
<h2>{{$t('settings.theme')}}</h2>
<style-switcher></style-switcher>
</div>
<div class="setting-item">
<h2>{{$t('settings.filtering')}}</h2>
<p>{{$t('settings.filtering_explanation')}}</p>
<textarea id="muteWords" v-model="muteWordsString"></textarea>
</div>
<div class="setting-item">
<h2>{{$t('nav.timeline')}}</h2>
<ul class="setting-list">
<li>
<input type="checkbox" id="collapseMessageWithSubject" v-model="collapseMessageWithSubjectLocal">
<label for="collapseMessageWithSubject">{{$t('settings.collapse_subject')}}</label>
</li>
<li>
<input type="checkbox" id="streaming" v-model="streamingLocal">
<label for="streaming">{{$t('settings.streaming')}}</label>
<ul class="setting-list suboptions" :class="[{disabled: !streamingLocal}]">
<tab-switcher>
<div :label="$t('settings.general')" >
<div class="setting-item">
<h2>{{ $t('settings.interfaceLanguage') }}</h2>
<interface-language-switcher />
</div>
<div class="setting-item">
<h2>{{$t('nav.timeline')}}</h2>
<ul class="setting-list">
<li>
<input :disabled="!streamingLocal" type="checkbox" id="pauseOnUnfocused" v-model="pauseOnUnfocusedLocal">
<label for="pauseOnUnfocused">{{$t('settings.pause_on_unfocused')}}</label>
<input type="checkbox" id="collapseMessageWithSubject" v-model="collapseMessageWithSubjectLocal">
<label for="collapseMessageWithSubject">{{$t('settings.collapse_subject')}}</label>
</li>
<li>
<input type="checkbox" id="streaming" v-model="streamingLocal">
<label for="streaming">{{$t('settings.streaming')}}</label>
<ul class="setting-list suboptions" :class="[{disabled: !streamingLocal}]">
<li>
<input :disabled="!streamingLocal" type="checkbox" id="pauseOnUnfocused" v-model="pauseOnUnfocusedLocal">
<label for="pauseOnUnfocused">{{$t('settings.pause_on_unfocused')}}</label>
</li>
</ul>
</li>
<li>
<input type="checkbox" id="autoload" v-model="autoLoadLocal">
<label for="autoload">{{$t('settings.autoload')}}</label>
</li>
<li>
<input type="checkbox" id="hoverPreview" v-model="hoverPreviewLocal">
<label for="hoverPreview">{{$t('settings.reply_link_preview')}}</label>
</li>
</ul>
</li>
<li>
<input type="checkbox" id="autoload" v-model="autoLoadLocal">
<label for="autoload">{{$t('settings.autoload')}}</label>
</li>
<li>
<input type="checkbox" id="hoverPreview" v-model="hoverPreviewLocal">
<label for="hoverPreview">{{$t('settings.reply_link_preview')}}</label>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{$t('settings.attachments')}}</h2>
<ul class="setting-list">
<li>
<input type="checkbox" id="hideAttachments" v-model="hideAttachmentsLocal">
<label for="hideAttachments">{{$t('settings.hide_attachments_in_tl')}}</label>
</li>
<li>
<input type="checkbox" id="hideAttachmentsInConv" v-model="hideAttachmentsInConvLocal">
<label for="hideAttachmentsInConv">{{$t('settings.hide_attachments_in_convo')}}</label>
</li>
<li>
<input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
<label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
</li>
<li>
<input type="checkbox" id="stopGifs" v-model="stopGifs">
<label for="stopGifs">{{$t('settings.stop_gifs')}}</label>
</li>
<li>
<input type="checkbox" id="loopVideo" v-model="loopVideoLocal">
<label for="loopVideo">{{$t('settings.loop_video')}}</label>
<ul class="setting-list suboptions" :class="[{disabled: !streamingLocal}]">
</div>
<div class="setting-item">
<h2>{{$t('settings.attachments')}}</h2>
<ul class="setting-list">
<li>
<input :disabled="!loopVideoLocal || !loopSilentAvailable" type="checkbox" id="loopVideoSilentOnly" v-model="loopVideoSilentOnlyLocal">
<label for="loopVideoSilentOnly">{{$t('settings.loop_video_silent_only')}}</label>
<div v-if="!loopSilentAvailable" class="unavailable">
<i class="icon-globe"/>! {{$t('settings.limited_availability')}}
</div>
<input type="checkbox" id="hideAttachments" v-model="hideAttachmentsLocal">
<label for="hideAttachments">{{$t('settings.hide_attachments_in_tl')}}</label>
</li>
<li>
<input type="checkbox" id="hideAttachmentsInConv" v-model="hideAttachmentsInConvLocal">
<label for="hideAttachmentsInConv">{{$t('settings.hide_attachments_in_convo')}}</label>
</li>
<li>
<input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
<label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
</li>
<li>
<input type="checkbox" id="stopGifs" v-model="stopGifs">
<label for="stopGifs">{{$t('settings.stop_gifs')}}</label>
</li>
<li>
<input type="checkbox" id="loopVideo" v-model="loopVideoLocal">
<label for="loopVideo">{{$t('settings.loop_video')}}</label>
<ul class="setting-list suboptions" :class="[{disabled: !streamingLocal}]">
<li>
<input :disabled="!loopVideoLocal || !loopSilentAvailable" type="checkbox" id="loopVideoSilentOnly" v-model="loopVideoSilentOnlyLocal">
<label for="loopVideoSilentOnly">{{$t('settings.loop_video_silent_only')}}</label>
<div v-if="!loopSilentAvailable" class="unavailable">
<i class="icon-globe"/>! {{$t('settings.limited_availability')}}
</div>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
<div :label="$t('settings.theme')" >
<div class="setting-item">
<style-switcher></style-switcher>
</div>
</div>
<div :label="$t('settings.filtering')" >
<div class="setting-item">
<div class="select-multiple">
<span class="label">{{$t('settings.notification_visibility')}}</span>
<ul class="option-list">
<li>
<input type="checkbox" id="notification-visibility-likes" v-model="notificationVisibilityLocal.likes">
<label for="notification-visibility-likes">
{{$t('settings.notification_visibility_likes')}}
</label>
</li>
<li>
<input type="checkbox" id="notification-visibility-repeats" v-model="notificationVisibilityLocal.repeats">
<label for="notification-visibility-repeats">
{{$t('settings.notification_visibility_repeats')}}
</label>
</li>
<li>
<input type="checkbox" id="notification-visibility-follows" v-model="notificationVisibilityLocal.follows">
<label for="notification-visibility-follows">
{{$t('settings.notification_visibility_follows')}}
</label>
</li>
<li>
<input type="checkbox" id="notification-visibility-mentions" v-model="notificationVisibilityLocal.mentions">
<label for="notification-visibility-mentions">
{{$t('settings.notification_visibility_mentions')}}
</label>
</li>
</ul>
</label>
</div>
<div>
{{$t('settings.replies_in_timeline')}}
<label for="replyVisibility" class="select">
<select id="replyVisibility" v-model="replyVisibilityLocal">
<option value="all" selected>{{$t('settings.reply_visibility_all')}}</option>
<option value="following">{{$t('settings.reply_visibility_following')}}</option>
<option value="self">{{$t('settings.reply_visibility_self')}}</option>
</select>
<i class="icon-down-open"/>
</label>
</div>
</div>
<div class="setting-item">
<p>{{$t('settings.filtering_explanation')}}</p>
<textarea id="muteWords" v-model="muteWordsString"></textarea>
</div>
</div>
</tab-switcher>
</div>
</div>
</template>
@ -89,6 +145,23 @@
margin: 1em 1em 1.4em;
padding-bottom: 1.4em;
> div {
margin-bottom: .5em;
&:last-child {
margin-bottom: 0;
}
}
&:last-child {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 1em;
}
select {
min-width: 10em;
}
textarea {
width: 100%;
@ -116,12 +189,24 @@
}
.btn {
margin-top: 1em;
min-height: 28px;
}
.submit {
margin-top: 1em;
min-height: 30px;
width: 10em;
}
}
.setting-list {
.select-multiple {
display: flex;
.option-list {
margin: 0;
padding-left: .5em;
}
}
.setting-list,
.option-list{
list-style-type: none;
padding-left: 2em;
li {

View File

@ -83,7 +83,6 @@ const Status = {
return hits
},
muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) },
isReply () { return !!this.status.in_reply_to_status_id },
isFocused () {
// retweet or root of an expanded conversation
if (this.focused) {
@ -105,6 +104,48 @@ const Status = {
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
return lengthScore > 20
},
isReply () {
if (this.status.in_reply_to_status_id) {
return true
}
// For private replies where we can't see the OP, in_reply_to_status_id will be null.
// So instead, check that the post starts with a @mention.
if (this.status.visibility === 'private') {
var textBody = this.status.text
if (this.status.summary !== null) {
textBody = textBody.substring(this.status.summary.length, textBody.length)
}
return textBody.startsWith('@')
}
return false
},
hideReply () {
if (this.$store.state.config.replyVisibility === 'all') {
return false
}
if (this.inlineExpanded || this.expanded || this.inConversation || !this.isReply) {
return false
}
if (this.status.user.id === this.$store.state.users.currentUser.id) {
return false
}
if (this.status.activity_type === 'repeat') {
return false
}
var checkFollowing = this.$store.state.config.replyVisibility === 'following'
for (var i = 0; i < this.status.attentions.length; ++i) {
if (this.status.user.id === this.status.attentions[i].id) {
continue
}
if (checkFollowing && this.status.attentions[i].following) {
return false
}
if (this.status.attentions[i].id === this.$store.state.users.currentUser.id) {
return false
}
}
return this.status.attentions.length > 0
},
hideSubjectStatus () {
if (this.tallStatus && !this.$store.state.config.collapseMessageWithSubject) {
return false
@ -123,6 +164,21 @@ const Status = {
showingMore () {
return this.showingTall || (this.status.summary && this.expandingSubject)
},
nsfwClickthrough () {
if (!this.status.nsfw) {
return false
}
if (this.status.summary && this.$store.state.config.collapseMessageWithSubject) {
return false
}
return true
},
replySubject () {
if (this.status.summary && !this.status.summary.match(/^re[: ]/i)) {
return 're: '.concat(this.status.summary)
}
return this.status.summary
},
attachmentSize () {
if ((this.$store.state.config.hideAttachments && !this.inConversation) ||
(this.$store.state.config.hideAttachmentsInConv && this.inConversation)) {
@ -226,6 +282,11 @@ const Status = {
}
}
}
},
filters: {
capitalize: function (str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
}
}

View File

@ -1,5 +1,5 @@
<template>
<div class="status-el" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
<div class="status-el" v-if="!hideReply" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
<template v-if="muted && !noReplyLinks">
<div class="media status container muted">
<small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
@ -58,11 +58,15 @@
<timeago :since="status.created_at" :auto-update="60"></timeago>
</router-link>
<div class="visibility-icon" v-if="status.visibility">
<i :class="visibilityIcon(status.visibility)"></i>
<i :class="visibilityIcon(status.visibility)" :title="status.visibility | capitalize"></i>
</div>
<a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="icon-link-ext-alt"></i></a>
<a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url" title="Source">
<i class="icon-link-ext-alt"></i>
</a>
<template v-if="expandable">
<a href="#" @click.prevent="toggleExpanded"><i class="icon-plus-squared"></i></a>
<a href="#" @click.prevent="toggleExpanded" title="Expand">
<i class="icon-plus-squared"></i>
</a>
</template>
<a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="icon-eye-off"></i></a>
</div>
@ -83,8 +87,8 @@
<a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">Show less</a>
</div>
<div v-if='status.attachments' class='attachments media-body'>
<attachment :size="attachmentSize" :status-id="status.id" :nsfw="status.nsfw" :attachment="attachment" v-for="attachment in status.attachments" :key="attachment.id">
<div v-if='status.attachments && !hideSubjectStatus' class='attachments media-body'>
<attachment :size="attachmentSize" :status-id="status.id" :nsfw="nsfwClickthrough" :attachment="attachment" v-for="attachment in status.attachments" :key="attachment.id">
</attachment>
</div>
@ -102,7 +106,7 @@
</div>
<div class="container" v-if="replying">
<div class="reply-left"/>
<post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" :message-scope="status.visibility" v-on:posted="toggleReplying"/>
<post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" :message-scope="status.visibility" :subject="replySubject" v-on:posted="toggleReplying"/>
</div>
</template>
</div>
@ -331,11 +335,35 @@
font-style: italic;
}
pre {
overflow: auto;
}
p {
margin: 0;
margin-top: 0.2em;
margin-bottom: 0.5em;
}
h1 {
font-size: 1.1em;
line-height: 1.2em;
margin: 1.4em 0;
}
h2 {
font-size: 1.1em;
margin: 1.0em 0;
}
h3 {
font-size: 1em;
margin: 1.2em 0;
}
h4 {
margin: 1.1em 0;
}
}
.retweet-info {

View File

@ -18,7 +18,11 @@ const StillImage = {
onLoad () {
const canvas = this.$refs.canvas
if (!canvas) return
canvas.getContext('2d').drawImage(this.$refs.src, 1, 1, canvas.width, canvas.height)
const width = this.$refs.src.naturalWidth
const height = this.$refs.src.naturalHeight
canvas.width = width
canvas.height = height
canvas.getContext('2d').drawImage(this.$refs.src, 0, 0, width, height)
}
}
}

View File

@ -23,6 +23,7 @@
img {
width: 100%;
height: 100%;
object-fit: contain;
}
&.animated {
@ -60,6 +61,7 @@
right: 0;
width: 100%;
height: 100%;
object-fit: contain;
}
}
</style>

View File

@ -1,102 +1,30 @@
<template>
<div>
<div>{{$t('settings.presets')}}
<div>
<div class="presets-container">
<div>
{{$t('settings.presets')}}
<label for="style-switcher" class='select'>
<select id="style-switcher" v-model="selected" class="style-switcher">
<option v-for="style in availableStyles" :value="style" :style="{
backgroundColor: style[1],
color: style[3]
}">{{style[0]}}</option>
<option v-for="style in availableStyles"
:value="style"
:style="{
backgroundColor: style[1],
color: style[3]
}">
{{style[0]}}
</option>
</select>
<i class="icon-down-open"/>
</label>
</div>
<div>
<div class="import-export">
<button class="btn" @click="exportCurrentTheme">{{ $t('settings.export_theme') }}</button>
<button class="btn" @click="importTheme">{{ $t('settings.import_theme') }}</button>
<p v-if="invalidThemeImported" class="import-warning">{{ $t('settings.invalid_theme_imported') }}</p>
</div>
<div class="color-container">
<p>{{$t('settings.theme_help')}}</p>
<div class="color-item">
<label for="bgcolor" class="theme-color-lb">{{$t('settings.background')}}</label>
<input id="bgcolor" class="theme-color-cl" type="color" v-model="bgColorLocal">
<input id="bgcolor-t" class="theme-color-in" type="text" v-model="bgColorLocal">
</div>
<div class="color-item">
<label for="fgcolor" class="theme-color-lb">{{$t('settings.foreground')}}</label>
<input id="fgcolor" class="theme-color-cl" type="color" v-model="btnColorLocal">
<input id="fgcolor-t" class="theme-color-in" type="text" v-model="btnColorLocal">
</div>
<div class="color-item">
<label for="textcolor" class="theme-color-lb">{{$t('settings.text')}}</label>
<input id="textcolor" class="theme-color-cl" type="color" v-model="textColorLocal">
<input id="textcolor-t" class="theme-color-in" type="text" v-model="textColorLocal">
</div>
<div class="color-item">
<label for="linkcolor" class="theme-color-lb">{{$t('settings.links')}}</label>
<input id="linkcolor" class="theme-color-cl" type="color" v-model="linkColorLocal">
<input id="linkcolor-t" class="theme-color-in" type="text" v-model="linkColorLocal">
</div>
<div class="color-item">
<label for="redcolor" class="theme-color-lb">{{$t('settings.cRed')}}</label>
<input id="redcolor" class="theme-color-cl" type="color" v-model="redColorLocal">
<input id="redcolor-t" class="theme-color-in" type="text" v-model="redColorLocal">
</div>
<div class="color-item">
<label for="bluecolor" class="theme-color-lb">{{$t('settings.cBlue')}}</label>
<input id="bluecolor" class="theme-color-cl" type="color" v-model="blueColorLocal">
<input id="bluecolor-t" class="theme-color-in" type="text" v-model="blueColorLocal">
</div>
<div class="color-item">
<label for="greencolor" class="theme-color-lb">{{$t('settings.cGreen')}}</label>
<input id="greencolor" class="theme-color-cl" type="color" v-model="greenColorLocal">
<input id="greencolor-t" class="theme-color-in" type="green" v-model="greenColorLocal">
</div>
<div class="color-item">
<label for="orangecolor" class="theme-color-lb">{{$t('settings.cOrange')}}</label>
<input id="orangecolor" class="theme-color-cl" type="color" v-model="orangeColorLocal">
<input id="orangecolor-t" class="theme-color-in" type="text" v-model="orangeColorLocal">
</div>
</div>
<div class="radius-container">
<p>{{$t('settings.radii_help')}}</p>
<div class="radius-item">
<label for="btnradius" class="theme-radius-lb">{{$t('settings.btnRadius')}}</label>
<input id="btnradius" class="theme-radius-rn" type="range" v-model="btnRadiusLocal" max="16">
<input id="btnradius-t" class="theme-radius-in" type="text" v-model="btnRadiusLocal">
</div>
<div class="radius-item">
<label for="inputradius" class="theme-radius-lb">{{$t('settings.inputRadius')}}</label>
<input id="inputradius" class="theme-radius-rn" type="range" v-model="inputRadiusLocal" max="16">
<input id="inputradius-t" class="theme-radius-in" type="text" v-model="inputRadiusLocal">
</div>
<div class="radius-item">
<label for="panelradius" class="theme-radius-lb">{{$t('settings.panelRadius')}}</label>
<input id="panelradius" class="theme-radius-rn" type="range" v-model="panelRadiusLocal" max="50">
<input id="panelradius-t" class="theme-radius-in" type="text" v-model="panelRadiusLocal">
</div>
<div class="radius-item">
<label for="avatarradius" class="theme-radius-lb">{{$t('settings.avatarRadius')}}</label>
<input id="avatarradius" class="theme-radius-rn" type="range" v-model="avatarRadiusLocal" max="28">
<input id="avatarradius-t" class="theme-radius-in" type="green" v-model="avatarRadiusLocal">
</div>
<div class="radius-item">
<label for="avataraltradius" class="theme-radius-lb">{{$t('settings.avatarAltRadius')}}</label>
<input id="avataraltradius" class="theme-radius-rn" type="range" v-model="avatarAltRadiusLocal" max="28">
<input id="avataraltradius-t" class="theme-radius-in" type="text" v-model="avatarAltRadiusLocal">
</div>
<div class="radius-item">
<label for="attachmentradius" class="theme-radius-lb">{{$t('settings.attachmentRadius')}}</label>
<input id="attachmentrradius" class="theme-radius-rn" type="range" v-model="attachmentRadiusLocal" max="50">
<input id="attachmentradius-t" class="theme-radius-in" type="text" v-model="attachmentRadiusLocal">
</div>
<div class="radius-item">
<label for="tooltipradius" class="theme-radius-lb">{{$t('settings.tooltipRadius')}}</label>
<input id="tooltipradius" class="theme-radius-rn" type="range" v-model="tooltipRadiusLocal" max="20">
<input id="tooltipradius-t" class="theme-radius-in" type="text" v-model="tooltipRadiusLocal">
</div>
</div>
</div>
<div class="preview-container">
<div :style="{
'--btnRadius': btnRadiusLocal + 'px',
'--inputRadius': inputRadiusLocal + 'px',
@ -127,8 +55,95 @@
</div>
</div>
</div>
<button class="btn" @click="setCustomTheme">{{$t('general.apply')}}</button>
</div>
<div class="color-container">
<p>{{$t('settings.theme_help')}}</p>
<div class="color-item">
<label for="bgcolor" class="theme-color-lb">{{$t('settings.background')}}</label>
<input id="bgcolor" class="theme-color-cl" type="color" v-model="bgColorLocal">
<input id="bgcolor-t" class="theme-color-in" type="text" v-model="bgColorLocal">
</div>
<div class="color-item">
<label for="fgcolor" class="theme-color-lb">{{$t('settings.foreground')}}</label>
<input id="fgcolor" class="theme-color-cl" type="color" v-model="btnColorLocal">
<input id="fgcolor-t" class="theme-color-in" type="text" v-model="btnColorLocal">
</div>
<div class="color-item">
<label for="textcolor" class="theme-color-lb">{{$t('settings.text')}}</label>
<input id="textcolor" class="theme-color-cl" type="color" v-model="textColorLocal">
<input id="textcolor-t" class="theme-color-in" type="text" v-model="textColorLocal">
</div>
<div class="color-item">
<label for="linkcolor" class="theme-color-lb">{{$t('settings.links')}}</label>
<input id="linkcolor" class="theme-color-cl" type="color" v-model="linkColorLocal">
<input id="linkcolor-t" class="theme-color-in" type="text" v-model="linkColorLocal">
</div>
<div class="color-item">
<label for="redcolor" class="theme-color-lb">{{$t('settings.cRed')}}</label>
<input id="redcolor" class="theme-color-cl" type="color" v-model="redColorLocal">
<input id="redcolor-t" class="theme-color-in" type="text" v-model="redColorLocal">
</div>
<div class="color-item">
<label for="bluecolor" class="theme-color-lb">{{$t('settings.cBlue')}}</label>
<input id="bluecolor" class="theme-color-cl" type="color" v-model="blueColorLocal">
<input id="bluecolor-t" class="theme-color-in" type="text" v-model="blueColorLocal">
</div>
<div class="color-item">
<label for="greencolor" class="theme-color-lb">{{$t('settings.cGreen')}}</label>
<input id="greencolor" class="theme-color-cl" type="color" v-model="greenColorLocal">
<input id="greencolor-t" class="theme-color-in" type="green" v-model="greenColorLocal">
</div>
<div class="color-item">
<label for="orangecolor" class="theme-color-lb">{{$t('settings.cOrange')}}</label>
<input id="orangecolor" class="theme-color-cl" type="color" v-model="orangeColorLocal">
<input id="orangecolor-t" class="theme-color-in" type="text" v-model="orangeColorLocal">
</div>
</div>
<div class="radius-container">
<p>{{$t('settings.radii_help')}}</p>
<div class="radius-item">
<label for="btnradius" class="theme-radius-lb">{{$t('settings.btnRadius')}}</label>
<input id="btnradius" class="theme-radius-rn" type="range" v-model="btnRadiusLocal" max="16">
<input id="btnradius-t" class="theme-radius-in" type="text" v-model="btnRadiusLocal">
</div>
<div class="radius-item">
<label for="inputradius" class="theme-radius-lb">{{$t('settings.inputRadius')}}</label>
<input id="inputradius" class="theme-radius-rn" type="range" v-model="inputRadiusLocal" max="16">
<input id="inputradius-t" class="theme-radius-in" type="text" v-model="inputRadiusLocal">
</div>
<div class="radius-item">
<label for="panelradius" class="theme-radius-lb">{{$t('settings.panelRadius')}}</label>
<input id="panelradius" class="theme-radius-rn" type="range" v-model="panelRadiusLocal" max="50">
<input id="panelradius-t" class="theme-radius-in" type="text" v-model="panelRadiusLocal">
</div>
<div class="radius-item">
<label for="avatarradius" class="theme-radius-lb">{{$t('settings.avatarRadius')}}</label>
<input id="avatarradius" class="theme-radius-rn" type="range" v-model="avatarRadiusLocal" max="28">
<input id="avatarradius-t" class="theme-radius-in" type="green" v-model="avatarRadiusLocal">
</div>
<div class="radius-item">
<label for="avataraltradius" class="theme-radius-lb">{{$t('settings.avatarAltRadius')}}</label>
<input id="avataraltradius" class="theme-radius-rn" type="range" v-model="avatarAltRadiusLocal" max="28">
<input id="avataraltradius-t" class="theme-radius-in" type="text" v-model="avatarAltRadiusLocal">
</div>
<div class="radius-item">
<label for="attachmentradius" class="theme-radius-lb">{{$t('settings.attachmentRadius')}}</label>
<input id="attachmentrradius" class="theme-radius-rn" type="range" v-model="attachmentRadiusLocal" max="50">
<input id="attachmentradius-t" class="theme-radius-in" type="text" v-model="attachmentRadiusLocal">
</div>
<div class="radius-item">
<label for="tooltipradius" class="theme-radius-lb">{{$t('settings.tooltipRadius')}}</label>
<input id="tooltipradius" class="theme-radius-rn" type="range" v-model="tooltipRadiusLocal" max="20">
<input id="tooltipradius-t" class="theme-radius-in" type="text" v-model="tooltipRadiusLocal">
</div>
</div>
<div class="apply-container">
<button class="btn submit" @click="setCustomTheme">{{$t('general.apply')}}</button>
</div>
</div>
</template>
<script src="./style_switcher.js"></script>
@ -144,15 +159,19 @@
color: var(--cRed, $fallback--cRed);
}
.apply-container,
.radius-container,
.color-container {
.color-container,
.presets-container {
display: flex;
p {
flex: 2 0 100%;
margin-top: 2em;
margin-bottom: .5em;
}
}
.radius-container {
flex-direction: column;
}
@ -162,6 +181,36 @@
justify-content: space-between;
}
.presets-container {
justify-content: center;
.import-export {
display: flex;
.btn {
margin-left: .5em;
}
}
}
.preview-container {
border-top: 1px dashed;
border-bottom: 1px dashed;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
margin: 1em -1em 0;
padding: 1em;
.btn {
margin-top: 1em;
min-height: 30px;
width: 10em;
}
}
.apply-container {
justify-content: center;
}
.radius-item,
.color-item {
min-width: 20em;
@ -229,6 +278,7 @@
flex: 0;
min-width: 2em;
cursor: pointer;
max-height: 29px;
}
.theme-preview-content {

View File

@ -0,0 +1,44 @@
import Vue from 'vue'
import './tab_switcher.scss'
export default Vue.component('tab-switcher', {
name: 'TabSwitcher',
data () {
return {
active: 0
}
},
methods: {
activateTab(index) {
return () => this.active = index;
}
},
render(h) {
const tabs = this.$slots.default
.filter(slot => slot.data)
.map((slot, index) => {
const classes = ['tab']
if (index === this.active) {
classes.push('active')
}
return (<button onClick={this.activateTab(index)} class={ classes.join(' ') }>{slot.data.attrs.label}</button>)
});
const contents = (
<div>
{this.$slots.default.filter(slot => slot.data)[this.active]}
</div>
);
return (
<div class="tab-switcher">
<div class="tabs">
{tabs}
</div>
<div class="contents">
{contents}
</div>
</div>
)
}
})

View File

@ -0,0 +1,43 @@
@import '../../_variables.scss';
.tab-switcher {
.tabs {
display: flex;
position: relative;
justify-content: center;
width: 100%;
overflow: hidden;
padding-top: 5px;
&::after, &::before {
display: block;
content: '';
flex: 1 1 auto;
}
.tab, &::after, &::before {
border-bottom: 1px solid;
border-bottom-color: $fallback--btn;
border-bottom-color: var(--btn, $fallback--btn);
}
.tab {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
padding: .3em 1em;
&:not(.active) {
border-bottom: 1px solid;
border-bottom-color: $fallback--btn;
border-bottom-color: var(--btn, $fallback--btn);
z-index: 4;
}
&.active {
background: transparent;
border-bottom: none;
z-index: 5;
}
}
}
}

View File

@ -4,12 +4,12 @@
<div class="title">
{{title}}
</div>
<button @click.prevent="showNewStatuses" class="loadmore-button" v-if="timeline.newStatusCount > 0 && !timelineError">
{{$t('timeline.show_new')}}{{newStatusCountStr}}
</button>
<div @click.prevent class="loadmore-error alert error" v-if="timelineError">
{{$t('timeline.error_fetching')}}
</div>
<button @click.prevent="showNewStatuses" class="loadmore-button" v-if="timeline.newStatusCount > 0 && !timelineError">
{{$t('timeline.show_new')}}{{newStatusCountStr}}
</button>
<div @click.prevent class="loadmore-text" v-if="!timeline.newStatusCount > 0 && !timelineError">
{{$t('timeline.up_to_date')}}
</div>
@ -57,36 +57,7 @@
@import '../../_variables.scss';
.timeline {
.timeline-heading {
position: relative;
display: flex;
}
.title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 70%;
}
.loadmore-button {
position: absolute;
right: 0.6em;
font-size: 14px;
min-width: 6em;
height: 1.8em;
line-height: 100%;
}
.loadmore-text {
position: absolute;
right: 0.6em;
font-size: 14px;
min-width: 6em;
font-family: sans-serif;
text-align: center;
padding: 0 0.5em 0 0.5em;
opacity: 0.8;
background-color: transparent;
color: $fallback--faint;
@ -94,14 +65,6 @@
}
.loadmore-error {
position: absolute;
right: 0.6em;
font-size: 14px;
min-width: 6em;
font-family: sans-serif;
text-align: center;
padding: 0 0.25em 0 0.25em;
margin: 0;
color: $fallback--fg;
color: var(--fg, $fallback--fg);
}

View File

@ -73,12 +73,14 @@
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-color: var(--border, $fallback--border);
border-width: 1px;
overflow: hidden;
.panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
p {

View File

@ -105,8 +105,8 @@
<span>{{user.followers_count}}</span>
</div>
</div>
<p v-if="!hideBio && user.description_html" v-html="user.description_html"></p>
<p v-else-if="!hideBio">{{ user.description }}</p>
<p v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p>
<p v-else-if="!hideBio" class="profile-bio">{{ user.description }}</p>
</div>
</div>
</template>
@ -130,7 +130,11 @@
.profile-panel-body {
word-wrap: break-word;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%);
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%)
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%);
.profile-bio {
text-align: center;
}
}
.user-info {

View File

@ -14,8 +14,10 @@
<style lang="scss">
.user-panel {
.profile-panel-background .panel-heading {
background: transparent;
}
.profile-panel-background .panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
}
</style>

View File

@ -17,6 +17,8 @@
padding-bottom: 10px;
.panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
}
</style>

View File

@ -1,3 +1,4 @@
import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
import StyleSwitcher from '../style_switcher/style_switcher.vue'
const UserSettings = {
@ -23,7 +24,8 @@ const UserSettings = {
}
},
components: {
StyleSwitcher
StyleSwitcher,
TabSwitcher
},
computed: {
user () {

View File

@ -4,126 +4,131 @@
{{$t('settings.user_settings')}}
</div>
<div class="panel-body profile-edit">
<div class="tab-switcher">
<button class="btn btn-default" @click="activateTab('profile')">{{$t('settings.profile_tab')}}</button>
<button class="btn btn-default" @click="activateTab('security')">{{$t('settings.security_tab')}}</button>
<button class="btn btn-default" @click="activateTab('data_import_export')" v-if="pleromaBackend">{{$t('settings.data_import_export_tab')}}</button>
</div>
<div class="setting-item" v-if="activeTab == 'profile'">
<h2>{{$t('settings.name_bio')}}</h2>
<p>{{$t('settings.name')}}</p>
<input class='name-changer' id='username' v-model="newname"></input>
<p>{{$t('settings.bio')}}</p>
<textarea class="bio" v-model="newbio"></textarea>
<p>
<input type="checkbox" v-model="newlocked" id="account-locked">
<label for="account-locked">{{$t('settings.lock_account_description')}}</label>
</p>
<div v-if="scopeOptionsEnabled">
<label for="default-vis">{{$t('settings.default_vis')}}</label>
<div id="default-vis" class="visibility-tray">
<i v-on:click="changeVis('direct')" class="icon-mail-alt" :class="vis.direct"></i>
<i v-on:click="changeVis('private')" class="icon-lock" :class="vis.private"></i>
<i v-on:click="changeVis('unlisted')" class="icon-lock-open-alt" :class="vis.unlisted"></i>
<i v-on:click="changeVis('public')" class="icon-globe" :class="vis.public"></i>
<tab-switcher>
<div :label="$t('settings.profile_tab')">
<div class="setting-item" >
<h2>{{$t('settings.name_bio')}}</h2>
<p>{{$t('settings.name')}}</p>
<input class='name-changer' id='username' v-model="newname"></input>
<p>{{$t('settings.bio')}}</p>
<textarea class="bio" v-model="newbio"></textarea>
<p>
<input type="checkbox" v-model="newlocked" id="account-locked">
<label for="account-locked">{{$t('settings.lock_account_description')}}</label>
</p>
<div v-if="scopeOptionsEnabled">
<label for="default-vis">{{$t('settings.default_vis')}}</label>
<div id="default-vis" class="visibility-tray">
<i v-on:click="changeVis('direct')" class="icon-mail-alt" :class="vis.direct"></i>
<i v-on:click="changeVis('private')" class="icon-lock" :class="vis.private"></i>
<i v-on:click="changeVis('unlisted')" class="icon-lock-open-alt" :class="vis.unlisted"></i>
<i v-on:click="changeVis('public')" class="icon-globe" :class="vis.public"></i>
</div>
</div>
<button :disabled='newname.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
</div>
<div class="setting-item">
<h2>{{$t('settings.avatar')}}</h2>
<p>{{$t('settings.current_avatar')}}</p>
<img :src="user.profile_image_url_original" class="old-avatar"></img>
<p>{{$t('settings.set_new_avatar')}}</p>
<img class="new-avatar" v-bind:src="previews[0]" v-if="previews[0]">
</img>
<div>
<input type="file" @change="uploadFile(0, $event)" ></input>
</div>
<i class="icon-spin4 animate-spin" v-if="uploading[0]"></i>
<button class="btn btn-default" v-else-if="previews[0]" @click="submitAvatar">{{$t('general.submit')}}</button>
</div>
<div class="setting-item">
<h2>{{$t('settings.profile_banner')}}</h2>
<p>{{$t('settings.current_profile_banner')}}</p>
<img :src="user.cover_photo" class="banner"></img>
<p>{{$t('settings.set_new_profile_banner')}}</p>
<img class="banner" v-bind:src="previews[1]" v-if="previews[1]">
</img>
<div>
<input type="file" @change="uploadFile(1, $event)" ></input>
</div>
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[1]"></i>
<button class="btn btn-default" v-else-if="previews[1]" @click="submitBanner">{{$t('general.submit')}}</button>
</div>
<div class="setting-item">
<h2>{{$t('settings.profile_background')}}</h2>
<p>{{$t('settings.set_new_profile_background')}}</p>
<img class="bg" v-bind:src="previews[2]" v-if="previews[2]">
</img>
<div>
<input type="file" @change="uploadFile(2, $event)" ></input>
</div>
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[2]"></i>
<button class="btn btn-default" v-else-if="previews[2]" @click="submitBg">{{$t('general.submit')}}</button>
</div>
</div>
<button :disabled='newname.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
</div>
<div class="setting-item" v-if="activeTab == 'profile'">
<h2>{{$t('settings.avatar')}}</h2>
<p>{{$t('settings.current_avatar')}}</p>
<img :src="user.profile_image_url_original" class="old-avatar"></img>
<p>{{$t('settings.set_new_avatar')}}</p>
<img class="new-avatar" v-bind:src="previews[0]" v-if="previews[0]">
</img>
<div>
<input type="file" @change="uploadFile(0, $event)" ></input>
<div :label="$t('settings.security_tab')">
<div class="setting-item">
<h2>{{$t('settings.change_password')}}</h2>
<div>
<p>{{$t('settings.current_password')}}</p>
<input type="password" v-model="changePasswordInputs[0]">
</div>
<div>
<p>{{$t('settings.new_password')}}</p>
<input type="password" v-model="changePasswordInputs[1]">
</div>
<div>
<p>{{$t('settings.confirm_new_password')}}</p>
<input type="password" v-model="changePasswordInputs[2]">
</div>
<button class="btn btn-default" @click="changePassword">{{$t('general.submit')}}</button>
<p v-if="changedPassword">{{$t('settings.changed_password')}}</p>
<p v-else-if="changePasswordError !== false">{{$t('settings.change_password_error')}}</p>
<p v-if="changePasswordError">{{changePasswordError}}</p>
</div>
<div class="setting-item">
<h2>{{$t('settings.delete_account')}}</h2>
<p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
<div v-if="deletingAccount">
<p>{{$t('settings.delete_account_instructions')}}</p>
<p>{{$t('login.password')}}</p>
<input type="password" v-model="deleteAccountConfirmPasswordInput">
<button class="btn btn-default" @click="deleteAccount">{{$t('settings.delete_account')}}</button>
</div>
<p v-if="deleteAccountError !== false">{{$t('settings.delete_account_error')}}</p>
<p v-if="deleteAccountError">{{deleteAccountError}}</p>
<button class="btn btn-default" v-if="!deletingAccount" @click="confirmDelete">{{$t('general.submit')}}</button>
</div>
</div>
<i class="icon-spin4 animate-spin" v-if="uploading[0]"></i>
<button class="btn btn-default" v-else-if="previews[0]" @click="submitAvatar">{{$t('general.submit')}}</button>
</div>
<div class="setting-item" v-if="activeTab == 'profile'">
<h2>{{$t('settings.profile_banner')}}</h2>
<p>{{$t('settings.current_profile_banner')}}</p>
<img :src="user.cover_photo" class="banner"></img>
<p>{{$t('settings.set_new_profile_banner')}}</p>
<img class="banner" v-bind:src="previews[1]" v-if="previews[1]">
</img>
<div>
<input type="file" @change="uploadFile(1, $event)" ></input>
<div :label="$t('settings.data_import_export_tab')" v-if="pleromaBackend">
<div class="setting-item">
<h2>{{$t('settings.follow_import')}}</h2>
<p>{{$t('settings.import_followers_from_a_csv_file')}}</p>
<form v-model="followImportForm">
<input type="file" ref="followlist" v-on:change="followListChange"></input>
</form>
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[3]"></i>
<button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button>
<div v-if="followsImported">
<i class="icon-cross" @click="dismissImported"></i>
<p>{{$t('settings.follows_imported')}}</p>
</div>
<div v-else-if="followImportError">
<i class="icon-cross" @click="dismissImported"></i>
<p>{{$t('settings.follow_import_error')}}</p>
</div>
</div>
<div class="setting-item" v-if="enableFollowsExport">
<h2>{{$t('settings.follow_export')}}</h2>
<button class="btn btn-default" @click="exportFollows">{{$t('settings.follow_export_button')}}</button>
</div>
<div class="setting-item" v-else>
<h2>{{$t('settings.follow_export_processing')}}</h2>
</div>
</div>
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[1]"></i>
<button class="btn btn-default" v-else-if="previews[1]" @click="submitBanner">{{$t('general.submit')}}</button>
</div>
<div class="setting-item" v-if="activeTab == 'profile'">
<h2>{{$t('settings.profile_background')}}</h2>
<p>{{$t('settings.set_new_profile_background')}}</p>
<img class="bg" v-bind:src="previews[2]" v-if="previews[2]">
</img>
<div>
<input type="file" @change="uploadFile(2, $event)" ></input>
</div>
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[2]"></i>
<button class="btn btn-default" v-else-if="previews[2]" @click="submitBg">{{$t('general.submit')}}</button>
</div>
<div class="setting-item" v-if="activeTab == 'security'">
<h2>{{$t('settings.change_password')}}</h2>
<div>
<p>{{$t('settings.current_password')}}</p>
<input type="password" v-model="changePasswordInputs[0]">
</div>
<div>
<p>{{$t('settings.new_password')}}</p>
<input type="password" v-model="changePasswordInputs[1]">
</div>
<div>
<p>{{$t('settings.confirm_new_password')}}</p>
<input type="password" v-model="changePasswordInputs[2]">
</div>
<button class="btn btn-default" @click="changePassword">{{$t('general.submit')}}</button>
<p v-if="changedPassword">{{$t('settings.changed_password')}}</p>
<p v-else-if="changePasswordError !== false">{{$t('settings.change_password_error')}}</p>
<p v-if="changePasswordError">{{changePasswordError}}</p>
</div>
<div class="setting-item" v-if="pleromaBackend && activeTab == 'data_import_export'">
<h2>{{$t('settings.follow_import')}}</h2>
<p>{{$t('settings.import_followers_from_a_csv_file')}}</p>
<form v-model="followImportForm">
<input type="file" ref="followlist" v-on:change="followListChange"></input>
</form>
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[3]"></i>
<button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button>
<div v-if="followsImported">
<i class="icon-cross" @click="dismissImported"></i>
<p>{{$t('settings.follows_imported')}}</p>
</div>
<div v-else-if="followImportError">
<i class="icon-cross" @click="dismissImported"></i>
<p>{{$t('settings.follow_import_error')}}</p>
</div>
</div>
<div class="setting-item" v-if="enableFollowsExport && activeTab == 'data_import_export'">
<h2>{{$t('settings.follow_export')}}</h2>
<button class="btn btn-default" @click="exportFollows">{{$t('settings.follow_export_button')}}</button>
</div>
<div class="setting-item" v-else-if="activeTab == 'data_import_export'">
<h2>{{$t('settings.follow_export_processing')}}</h2>
</div>
<hr>
<div class="setting-item" v-if="activeTab == 'security'">
<h2>{{$t('settings.delete_account')}}</h2>
<p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
<div v-if="deletingAccount">
<p>{{$t('settings.delete_account_instructions')}}</p>
<p>{{$t('login.password')}}</p>
<input type="password" v-model="deleteAccountConfirmPasswordInput">
<button class="btn btn-default" @click="deleteAccount">{{$t('settings.delete_account')}}</button>
</div>
<p v-if="deleteAccountError !== false">{{$t('settings.delete_account_error')}}</p>
<p v-if="deleteAccountError">{{deleteAccountError}}</p>
<button class="btn btn-default" v-if="!deletingAccount" @click="confirmDelete">{{$t('general.submit')}}</button>
</div>
</tab-switcher>
</div>
</div>
</template>
@ -151,13 +156,4 @@
margin: 0.25em;
}
}
.tab-switcher {
margin: 7px 7px;
display: inline-block;
button {
height: 30px;
}
}
</style>

View File

@ -1,18 +1,21 @@
function showWhoToFollow (panel, reply, aHost, aUser) {
var users = reply.ids
import apiService from '../../services/api/api.service.js'
function showWhoToFollow (panel, reply) {
var users = reply
var cn
var index = 0
var random = Math.floor(Math.random() * 10)
for (cn = random; cn < users.length; cn = cn + 10) {
var index
var step = 7
cn = Math.floor(Math.random() * step)
for (index = 0; index < 3; index++) {
var user
user = users[cn]
var img
if (user.icon) {
img = user.icon
if (user.avatar) {
img = user.avatar
} else {
img = '/images/avi.png'
}
var name = user.to_id
var name = user.acct
if (index === 0) {
panel.img1 = img
panel.name1 = name
@ -44,35 +47,20 @@ function showWhoToFollow (panel, reply, aHost, aUser) {
}
})
}
index = index + 1
if (index > 2) {
break
}
cn = (cn + step) % users.length
}
}
function getWhoToFollow (panel) {
var user = panel.$store.state.users.currentUser.screen_name
if (user) {
var credentials = panel.$store.state.users.currentUser.credentials
if (credentials) {
panel.name1 = 'Loading...'
panel.name2 = 'Loading...'
panel.name3 = 'Loading...'
var host = window.location.hostname
var whoToFollowProvider = panel.$store.state.config.whoToFollowProvider
var url
url = whoToFollowProvider.replace(/{{host}}/g, encodeURIComponent(host))
url = url.replace(/{{user}}/g, encodeURIComponent(user))
window.fetch(url, {mode: 'cors'}).then(function (response) {
if (response.ok) {
return response.json()
} else {
panel.name1 = ''
panel.name2 = ''
panel.name3 = ''
}
}).then(function (reply) {
showWhoToFollow(panel, reply, host, user)
})
apiService.suggestions({credentials: credentials})
.then((reply) => {
showWhoToFollow(panel, reply)
})
}
}
@ -95,26 +83,26 @@ const WhoToFollowPanel = {
moreUrl: function () {
var host = window.location.hostname
var user = this.user
var whoToFollowLink = this.$store.state.config.whoToFollowLink
var suggestionsWeb = this.$store.state.config.suggestionsWeb
var url
url = whoToFollowLink.replace(/{{host}}/g, encodeURIComponent(host))
url = suggestionsWeb.replace(/{{host}}/g, encodeURIComponent(host))
url = url.replace(/{{user}}/g, encodeURIComponent(user))
return url
},
showWhoToFollowPanel () {
return this.$store.state.config.showWhoToFollowPanel
suggestionsEnabled () {
return this.$store.state.config.suggestionsEnabled
}
},
watch: {
user: function (user, oldUser) {
if (this.showWhoToFollowPanel) {
if (this.suggestionsEnabled) {
getWhoToFollow(this)
}
}
},
mounted:
function () {
if (this.showWhoToFollowPanel) {
if (this.suggestionsEnabled) {
getWhoToFollow(this)
}
}

View File

@ -3,7 +3,7 @@
<div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background base04">
<div class="title">
Who to follow
{{$t('who_to_follow.who_to_follow')}}
</div>
</div>
<div class="panel-body who-to-follow">
@ -11,7 +11,7 @@
<img v-bind:src="img1"/> <router-link :to="{ name: 'user-profile', params: { id: id1 } }">{{ name1 }}</router-link><br>
<img v-bind:src="img2"/> <router-link :to="{ name: 'user-profile', params: { id: id2 } }">{{ name2 }}</router-link><br>
<img v-bind:src="img3"/> <router-link :to="{ name: 'user-profile', params: { id: id3 } }">{{ name3 }}</router-link><br>
<img v-bind:src="$store.state.config.logo"> <a v-bind:href="moreUrl" target="_blank">More</a>
<img v-bind:src="$store.state.config.logo"> <a v-bind:href="moreUrl" target="_blank">{{$t('who_to_follow.more')}}</a>
</p>
</div>
</div>

View File

@ -7,7 +7,8 @@ const de = {
timeline: 'Zeitleiste',
mentions: 'Erwähnungen',
public_tl: 'Lokale Zeitleiste',
twkn: 'Das gesamte Netzwerk'
twkn: 'Das gesamte Netzwerk',
friend_requests: 'Followanfragen'
},
user_card: {
follows_you: 'Folgt dir!',
@ -18,10 +19,12 @@ const de = {
statuses: 'Beiträge',
mute: 'Stummschalten',
muted: 'Stummgeschaltet',
followers: 'Folgende',
followers: 'Followers',
followees: 'Folgt',
per_day: 'pro Tag',
remote_follow: 'Remote Follow'
remote_follow: 'Folgen',
approve: 'Genehmigen',
deny: 'Ablehnen'
},
timeline: {
show_new: 'Zeige Neuere',
@ -39,17 +42,17 @@ const de = {
bio: 'Bio',
avatar: 'Avatar',
current_avatar: 'Dein derzeitiger Avatar',
set_new_avatar: 'Setze neuen Avatar',
set_new_avatar: 'Setze einen neuen Avatar',
profile_banner: 'Profil Banner',
current_profile_banner: 'Dein derzeitiger Profil Banner',
set_new_profile_banner: 'Setze neuen Profil Banner',
current_profile_banner: 'Der derzeitige Banner deines Profils',
set_new_profile_banner: 'Setze einen neuen Banner für dein Profil',
profile_background: 'Profil Hintergrund',
set_new_profile_background: 'Setze neuen Profil Hintergrund',
set_new_profile_background: 'Setze einen neuen Hintergrund für dein Profil',
settings: 'Einstellungen',
theme: 'Farbschema',
presets: 'Voreinstellungen',
export_theme: 'Aktuelles Theme exportieren',
import_theme: 'Gespeichertes Theme laden',
export_theme: 'Farbschema speichern',
import_theme: 'Farbschema laden',
invalid_theme_imported: 'Die ausgewählte Datei ist kein unterstütztes Pleroma-Theme. Keine Änderungen wurden vorgenommen.',
theme_help: 'Benutze HTML Farbcodes (#rrggbb) um dein Farbschema anzupassen',
radii_help: 'Kantenrundung (in Pixel) der Oberfläche anpassen',
@ -78,15 +81,15 @@ const de = {
autoload: 'Aktiviere automatisches Laden von älteren Beiträgen beim scrollen',
streaming: 'Aktiviere automatisches Laden (Streaming) von neuen Beiträgen',
reply_link_preview: 'Aktiviere reply-link Vorschau bei Maus-Hover',
follow_import: 'Folgeliste importieren',
import_followers_from_a_csv_file: 'Importiere Kontakte, denen du folgen möchtest, aus einer CSV-Datei',
follows_imported: 'Folgeliste importiert! Die Bearbeitung kann eine Zeit lang dauern.',
follow_import_error: 'Fehler beim importieren der Folgeliste',
follow_import: 'Followers importieren',
import_followers_from_a_csv_file: 'Importiere Follower, denen du folgen möchtest, aus einer CSV-Datei',
follows_imported: 'Followers importiert! Die Bearbeitung kann eine Zeit lang dauern.',
follow_import_error: 'Fehler beim importieren der Follower',
delete_account: 'Account löschen',
delete_account_description: 'Lösche deinen Account und alle deine Nachrichten dauerhaft.',
delete_account_instructions: 'Tippe dein Passwort unten in das Feld ein um die Löschung deines Accounts zu bestätigen.',
delete_account_description: 'Lösche deinen Account und alle deine Nachrichten unwiderruflich.',
delete_account_instructions: 'Tippe dein Passwort unten in das Feld ein, um die Löschung deines Accounts zu bestätigen.',
delete_account_error: 'Es ist ein Fehler beim löschen deines Accounts aufgetreten. Tritt dies weiterhin auf, wende dich an den Administrator der Instanz.',
follow_export: 'Folgeliste exportieren',
follow_export: 'Follower exportieren',
follow_export_processing: 'In Bearbeitung. Die Liste steht gleich zum herunterladen bereit.',
follow_export_button: 'Liste (.csv) erstellen',
change_password: 'Passwort ändern',
@ -94,7 +97,8 @@ const de = {
new_password: 'Neues Passwort',
confirm_new_password: 'Neues Passwort bestätigen',
changed_password: 'Passwort erfolgreich geändert!',
change_password_error: 'Es gab ein Problem bei der Änderung des Passworts.'
change_password_error: 'Es gab ein Problem bei der Änderung des Passworts.',
lock_account_description: 'Sperre deinen Account, um neue Follower zu genehmigen oder abzulehnen'
},
notifications: {
notifications: 'Benachrichtigungen',
@ -116,7 +120,8 @@ const de = {
fullname: 'Angezeigter Name',
email: 'Email',
bio: 'Bio',
password_confirm: 'Passwort bestätigen'
password_confirm: 'Passwort bestätigen',
token: 'Einladungsschlüssel'
},
post_status: {
posting: 'Veröffentlichen',
@ -127,7 +132,7 @@ const de = {
scope: {
public: 'Öffentlich - Beitrag an öffentliche Zeitleisten',
unlisted: 'Nicht gelistet - Nicht in öffentlichen Zeitleisten anzeigen',
private: 'Nur Folgende - Beitrag nur an Folgende',
private: 'Nur Follower - Beitrag nur für Follower sichtbar',
direct: 'Direkt - Beitrag nur an erwähnte Profile'
}
},
@ -273,9 +278,11 @@ const en = {
load_older: 'Load older statuses',
conversation: 'Conversation',
collapse: 'Collapse',
repeated: 'repeated'
repeated: 'repeated',
no_retweet_hint: 'Post is marked as followers-only or direct and cannot be repeated'
},
settings: {
general: 'General',
user_settings: 'User Settings',
name_bio: 'Name & Bio',
name: 'Name',
@ -291,8 +298,8 @@ const en = {
settings: 'Settings',
theme: 'Theme',
presets: 'Presets',
export_theme: 'Export current theme',
import_theme: 'Load saved theme',
export_theme: 'Save preset',
import_theme: 'Load preset',
theme_help: 'Use hex color codes (#rrggbb) to customize your color theme.',
invalid_theme_imported: 'The selected file is not a supported Pleroma theme. No changes to your theme were made.',
radii_help: 'Set up interface edge rounding (in pixels)',
@ -325,6 +332,15 @@ const en = {
loop_video: 'Loop videos',
loop_video_silent_only: 'Loop only videos without sound (i.e. Mastodon\'s "gifs")',
reply_link_preview: 'Enable reply-link preview on mouse hover',
replies_in_timeline: 'Replies in timeline',
reply_visibility_all: 'Show all replies',
reply_visibility_following: 'Only show replies directed at me or users I\'m following',
reply_visibility_self: 'Only show replies directed at me',
notification_visibility: 'Types of notifications to show',
notification_visibility_likes: 'Likes',
notification_visibility_mentions: 'Mentions',
notification_visibility_repeats: 'Repeats',
notification_visibility_follows: 'Follows',
follow_import: 'Follow import',
import_followers_from_a_csv_file: 'Import follows from a csv file',
follows_imported: 'Follows imported! Processing them will take a while.',
@ -347,14 +363,17 @@ const en = {
default_vis: 'Default visibility scope',
profile_tab: 'Profile',
security_tab: 'Security',
data_import_export_tab: 'Data Import / Export'
data_import_export_tab: 'Data Import / Export',
interfaceLanguage: 'Interface language'
},
notifications: {
notifications: 'Notifications',
read: 'Read!',
followed_you: 'followed you',
favorited_you: 'favorited your status',
repeated_you: 'repeated your status'
repeated_you: 'repeated your status',
broken_favorite: 'Unknown status, searching for it...',
load_older: 'Load older notifications'
},
login: {
login: 'Log in',
@ -379,11 +398,15 @@ const en = {
account_not_locked_warning: 'Your account is not {0}. Anyone can follow you to view your follower-only posts.',
account_not_locked_warning_link: 'locked',
direct_warning: 'This post will only be visible to all the mentioned users.',
attachments_sensitive: 'Mark attachments as sensitive',
scope: {
public: 'Public - Post to public timelines',
unlisted: 'Unlisted - Do not post to public timelines',
private: 'Followers-only - Post to followers only',
direct: 'Direct - Post to mentioned users only'
},
content_type: {
plain_text: 'Plain text'
}
},
finder: {
@ -396,19 +419,32 @@ const en = {
},
user_profile: {
timeline_title: 'User Timeline'
},
who_to_follow: {
who_to_follow: 'Who to follow',
more: 'More'
},
features_panel: {
title: 'Features',
chat: 'Chat',
gopher: 'Gopher',
who_to_follow: 'Who to follow',
media_proxy: 'Media proxy',
scope_options: 'Scope options',
text_limit: 'Text limit'
}
}
const eo = {
chat: {
title: 'Babilo'
title: 'Babilejo'
},
nav: {
chat: 'Loka babilo',
timeline: 'Tempovido',
chat: 'Loka babilejo',
timeline: 'Tempolinio',
mentions: 'Mencioj',
public_tl: 'Publika tempovido',
twkn: 'Tuta konata reto'
public_tl: 'Publika tempolinio',
twkn: 'La tuta konata reto'
},
user_card: {
follows_you: 'Abonas vin!',
@ -418,26 +454,26 @@ const eo = {
block: 'Bari',
statuses: 'Statoj',
mute: 'Silentigi',
muted: 'Silentigita',
muted: 'Silentigitaj',
followers: 'Abonantoj',
followees: 'Abonatoj',
per_day: 'tage',
remote_follow: 'Fora abono'
remote_follow: 'Fore aboni'
},
timeline: {
show_new: 'Montri novajn',
error_fetching: 'Eraro ĝisdatigante',
error_fetching: 'Eraro dum ĝisdatigo',
up_to_date: 'Ĝisdata',
load_older: 'Enlegi pli malnovajn statojn',
load_older: 'Montri pli malnovajn statojn',
conversation: 'Interparolo',
collapse: 'Maletendi',
repeated: 'ripetata'
},
settings: {
user_settings: 'Uzulaj agordoj',
name_bio: 'Nomo kaj prio',
user_settings: 'Uzantaj agordoj',
name_bio: 'Nomo kaj priskribo',
name: 'Nomo',
bio: 'Prio',
bio: 'Priskribo',
avatar: 'Profilbildo',
current_avatar: 'Via nuna profilbildo',
set_new_avatar: 'Agordi novan profilbildon',
@ -447,9 +483,9 @@ const eo = {
profile_background: 'Profila fono',
set_new_profile_background: 'Agordi novan profilan fonon',
settings: 'Agordoj',
theme: 'Haŭto',
presets: 'Antaŭmetaĵoj',
theme_help: 'Uzu deksesumajn kolorkodojn (#rrvvbb) por adapti vian koloran haŭton.',
theme: 'Etoso',
presets: 'Antaŭagordoj',
theme_help: 'Uzu deksesumajn kolorkodojn (#rrvvbb) por adapti vian koloran etoson.',
radii_help: 'Agordi fasadan rondigon de randoj (rastrumere)',
background: 'Fono',
foreground: 'Malfono',
@ -457,65 +493,65 @@ const eo = {
links: 'Ligiloj',
cBlue: 'Blua (Respondo, abono)',
cRed: 'Ruĝa (Nuligo)',
cOrange: 'Orange (Ŝato)',
cOrange: 'Oranĝa (Ŝato)',
cGreen: 'Verda (Kunhavigo)',
btnRadius: 'Butonoj',
panelRadius: 'Paneloj',
avatarRadius: 'Profilbildoj',
avatarAltRadius: 'Profilbildoj (Sciigoj)',
avatarAltRadius: 'Profilbildoj (sciigoj)',
tooltipRadius: 'Ŝpruchelpiloj/avertoj',
attachmentRadius: 'Kunsendaĵoj',
filtering: 'Filtrado',
filtering_explanation: 'Ĉiuj statoj kun tiuj ĉi vortoj silentiĝos, po unu linie',
attachments: 'Kunsendaĵoj',
hide_attachments_in_tl: 'Kaŝi kunsendaĵojn en tempovido',
hide_attachments_in_tl: 'Kaŝi kunsendaĵojn en tempolinio',
hide_attachments_in_convo: 'Kaŝi kunsendaĵojn en interparoloj',
nsfw_clickthrough: 'Ŝalti traklakan kaŝon de konsternaj kunsendaĵoj',
stop_gifs: 'Movi GIF-bildojn dum ŝvebo',
autoload: 'Ŝalti memfaran enlegadon ĉe subo de paĝo',
streaming: 'Ŝalti memfaran fluigon de novaj afiŝoj ĉe supro de paĝo',
autoload: 'Ŝalti memfaran ŝarĝadon ĉe subo de paĝo',
streaming: 'Ŝalti memfaran fluigon de novaj afiŝoj ĉe la supro de la paĝo',
reply_link_preview: 'Ŝalti respond-ligilan antaŭvidon dum ŝvebo',
follow_import: 'Abona enporto',
import_followers_from_a_csv_file: 'Enporti abonojn de CSV-dosiero',
import_followers_from_a_csv_file: 'Enporti abonojn el CSV-dosiero',
follows_imported: 'Abonoj enportiĝis! Traktado daŭros iom.',
follow_import_error: 'Eraro enportante abonojn'
},
notifications: {
notifications: 'Sciigoj',
read: 'Legita!',
read: 'Legite!',
followed_you: 'ekabonis vin',
favorited_you: 'ŝatis vian staton',
repeated_you: 'ripetis vian staton'
},
login: {
login: 'Saluti',
login: 'Ensaluti',
username: 'Salutnomo',
placeholder: 'ekz. zero_cool',
password: 'Pasvorto',
register: 'Registriĝi',
logout: 'Adiaŭi'
logout: 'Elsaluti'
},
registration: {
registration: 'Registriĝo',
fullname: 'Vidiga nomo',
email: 'Retpoŝtadreso',
bio: 'Prio',
bio: 'Priskribo',
password_confirm: 'Konfirmo de pasvorto'
},
post_status: {
posting: 'Afiŝanta',
default: 'Ĵus alvenis la universalan kongreson!'
posting: 'Afiŝante',
default: 'Ĵus alvenis al la Universala Kongreso!'
},
finder: {
find_user: 'Trovi uzulon',
error_fetching_user: 'Eraro alportante uzulon'
find_user: 'Trovi uzanton',
error_fetching_user: 'Eraro alportante uzanton'
},
general: {
submit: 'Sendi',
apply: 'Apliki'
},
user_profile: {
timeline_title: 'Uzula tempovido'
timeline_title: 'Uzanta tempolinio'
}
}
@ -779,115 +815,156 @@ const ja = {
chat: 'ローカルチャット',
timeline: 'タイムライン',
mentions: 'メンション',
public_tl: '公開タイムライン',
twkn: '接続しているすべてのネットワーク'
public_tl: 'パブリックタイムライン',
twkn: 'つながっているすべてのネットワーク',
friend_requests: 'Follow Requests'
},
user_card: {
follows_you: 'フォローされました!',
following: 'フォロー',
following: 'フォローしています',
follow: 'フォロー',
blocked: 'ブロック済み',
blocked: 'ブロックしています',
block: 'ブロック',
statuses: '投稿',
statuses: 'ステータス',
mute: 'ミュート',
muted: 'ミュート済み',
muted: 'ミュートしています!',
followers: 'フォロワー',
followees: 'フォロー',
per_day: '/日',
remote_follow: 'リモートフォロー'
remote_follow: 'リモートフォロー',
approve: 'Approve',
deny: 'Deny'
},
timeline: {
show_new: '更新',
error_fetching: '更新の取得中にエラーが発生しました。',
up_to_date: '最新',
load_older: '古い投稿を読み込む',
conversation: '会話',
collapse: '折り畳む',
show_new: 'よみこみ',
error_fetching: 'よみこみがエラーになりました。',
up_to_date: 'さいしん',
load_older: 'ふるいステータス',
conversation: 'スレッド',
collapse: 'たたむ',
repeated: 'リピート'
},
settings: {
user_settings: 'ユーザー設定',
name_bio: '名前とプロフィール',
name: '名前',
user_settings: 'ユーザーせってい',
name_bio: 'なまえとプロフィール',
name: 'なまえ',
bio: 'プロフィール',
avatar: 'アバター',
current_avatar: 'あなたの現在のアバター',
set_new_avatar: '新しいアバターを設定する',
current_avatar: 'いまのアバター',
set_new_avatar: 'あたらしいアバターをせっていする',
profile_banner: 'プロフィールバナー',
current_profile_banner: '現在のプロフィールバナー',
set_new_profile_banner: 'しいプロフィールバナーを設定する',
profile_background: 'プロフィールの背景',
set_new_profile_background: '新しいプロフィールの背景を設定する',
settings: '設定',
current_profile_banner: 'いまのプロフィールバナー',
set_new_profile_banner: 'あたらしいプロフィールバナーを設定する',
profile_background: 'プロフィールのバックグラウンド',
set_new_profile_background: 'あたらしいプロフィールのバックグラウンドをせっていする',
settings: 'せってい',
theme: 'テーマ',
presets: 'プリセット',
theme_help: '16進数カラーコード (#aabbcc) を使用してカラーテーマをカスタマイズ出来ます。',
radii_help: 'インターフェースの縁の丸さを設定する。',
background: '背景',
foreground: '前景',
text: '文字',
theme_help: 'カラーテーマをカスタマイズできます。',
radii_help: 'インターフェースのまるさをせっていする。',
background: 'バックグラウンド',
foreground: 'フォアグラウンド',
text: 'もじ',
links: 'リンク',
cBlue: '青 (返信, フォロー)',
cRed: ' (キャンセル)',
cOrange: 'オレンジ (お気に入り)',
cGreen: '緑 (リツイート)',
cBlue: 'あお (リプライ, フォロー)',
cRed: 'あか (キャンセル)',
cOrange: 'オレンジ (おきにいり)',
cGreen: 'みどり (リピート)',
btnRadius: 'ボタン',
inputRadius: 'Input fields',
panelRadius: 'パネル',
avatarRadius: 'アバター',
avatarAltRadius: 'アバター (通知)',
avatarAltRadius: 'アバター (つうち)',
tooltipRadius: 'ツールチップ/アラート',
attachmentRadius: 'ファイル',
filtering: 'フィルタリング',
filtering_explanation: 'これらの単語を含むすべてのものがミュートされます。1行に1つの単語を入力してください。',
filtering_explanation: 'これらのことばをふくむすべてのものがミュートされます。1行に1つのことばをかいてください。',
attachments: 'ファイル',
hide_attachments_in_tl: 'タイムラインのファイルをす。',
hide_attachments_in_convo: '会話の中のファイルを隠す。',
nsfw_clickthrough: 'NSFWファイルの非表示を有効にする。',
stop_gifs: 'カーソルを重ねた時にGIFを再生する。',
autoload: '下にスクロールした時に自動で読み込むようにする。',
streaming: '上までスクロールした時に自動でストリーミングされるようにする。',
reply_link_preview: 'マウスカーソルを重ねた時に返信のプレビューを表示するようにする。',
hide_attachments_in_tl: 'タイムラインのファイルをかくす。',
hide_attachments_in_convo: 'スレッドのファイルをかくす。',
nsfw_clickthrough: 'NSFWなファイルをかくす。',
stop_gifs: 'カーソルをかさねたとき、GIFをうごかす。',
autoload: 'したにスクロールしたとき、じどうてきによみこむ。',
streaming: 'うえまでスクロールしたとき、じどうてきにストリーミングする。',
reply_link_preview: 'カーソルをかさねたとき、リプライのプレビューをみる。',
follow_import: 'フォローインポート',
import_followers_from_a_csv_file: 'CSVファイルからフォローをインポートする。',
follows_imported: 'フォローがインポートされました!処理に少し時間がかかるかもしれません。',
follow_import_error: 'フォロワーのインポート中にエラーが発生しました。'
follows_imported: 'フォローがインポートされました! すこしじかんがかかるかもしれません。',
follow_import_error: 'フォローのインポートがエラーになりました。',
delete_account: 'アカウントをけす',
delete_account_description: 'あなたのアカウントとメッセージが、きえます。',
delete_account_instructions: 'ほんとうにアカウントをけしてもいいなら、パスワードをかいてください。',
delete_account_error: 'アカウントをけすことが、できなかったかもしれません。インスタンスのかんりしゃに、れんらくしてください。',
follow_export: 'フォローのエクスポート',
follow_export_processing: 'おまちください。まもなくファイルをダウンロードできます。',
follow_export_button: 'エクスポート',
change_password: 'パスワードをかえる',
current_password: 'いまのパスワード',
new_password: 'あたらしいパスワード',
confirm_new_password: 'あたらしいパスワードのかくにん',
changed_password: 'パスワードが、かわりました!',
change_password_error: 'パスワードをかえることが、できなかったかもしれません。',
lock_account_description: 'あなたがみとめたひとだけ、あなたのアカウントをフォローできます。'
},
notifications: {
notifications: '通知',
read: '読んだ!',
notifications: 'つうち',
read: 'んだ!',
followed_you: 'フォローされました',
favorited_you: 'あなたの投稿がお気に入りされました',
repeated_you: 'あなたの投稿がリピートされました'
favorited_you: 'あなたのステータスがおきにいりされました',
repeated_you: 'あなたのステータスがリピートされました'
},
login: {
login: 'ログイン',
username: 'ユーザー名',
placeholder: '例えば zero_cool',
username: 'ユーザーめい',
placeholder: 'れい: zero_cool',
password: 'パスワード',
register: '登録',
register: 'はじめる',
logout: 'ログアウト'
},
registration: {
registration: '登録',
fullname: '表示名',
registration: 'はじめる',
fullname: 'スクリーンネーム',
email: 'Eメール',
bio: 'プロフィール',
password_confirm: 'パスワードの確認'
password_confirm: 'パスワードのかくにん'
},
post_status: {
posting: '投稿',
default: 'ちょうどL.A.に着陸しました。'
posting: 'とうこう',
content_warning: 'せつめい (かかなくてもよい)',
default: 'はねだくうこうに、つきました。',
account_not_locked_warning: 'あなたのアカウントは {0} ではありません。あなたをフォローすれば、だれでも、フォロワーげんていのステータスをよむことができます。',
account_not_locked_warning_link: 'ロックされたアカウント',
direct_warning: 'このステータスは、メンションされたユーザーだけが、よむことができます。',
scope: {
public: 'パブリック - パブリックタイムラインにとどきます。',
unlisted: 'アンリステッド - パブリックタイムラインにとどきません。',
private: 'フォロワーげんてい - フォロワーのみにとどきます。',
direct: 'ダイレクト - メンションされたユーザーのみにとどきます。'
}
},
finder: {
find_user: 'ユーザー検索',
error_fetching_user: 'ユーザー検索でエラーが発生しました'
find_user: 'ユーザーをさがす',
error_fetching_user: 'ユーザーけんさくがエラーになりました。'
},
general: {
submit: '送信',
apply: '適用'
submit: 'そうしん',
apply: 'てきよう'
},
user_profile: {
timeline_title: 'ユーザータイムライン'
},
who_to_follow: {
who_to_follow: 'おすすめユーザー',
more: 'くわしく'
},
features_panel: {
title: 'ゆうこうなきのう',
chat: 'チャット',
gopher: 'Gopher',
who_to_follow: 'おすすめユーザー',
media_proxy: 'メディアプロクシ',
scope_options: 'こうかいはんい',
text_limit: 'もじのかず'
}
}
@ -1099,8 +1176,8 @@ const oc = {
twkn: 'Lo malhum conegut'
},
user_card: {
follows_you: 'Vos sèc!',
following: 'Seguit!',
follows_you: 'Vos sèc!',
following: 'Seguit!',
follow: 'Seguir',
blocked: 'Blocat',
block: 'Blocar',
@ -1145,10 +1222,10 @@ const oc = {
links: 'Ligams',
cBlue: 'Blau (Respondre, seguir)',
cRed: 'Roge (Anullar)',
cOrange: 'Irange (Metre en favorit)',
cOrange: 'Irange (Aimar)',
cGreen: 'Verd (Repartajar)',
inputRadius: 'Camps tèxte',
btnRadius: 'Botons',
inputRadius: 'Camps tèxte',
panelRadius: 'Panèls',
avatarRadius: 'Avatars',
avatarAltRadius: 'Avatars (Notificacions)',
@ -1167,12 +1244,25 @@ const oc = {
follow_import: 'Importar los abonaments',
import_followers_from_a_csv_file: 'Importar los seguidors dun fichièr csv',
follows_imported: 'Seguidors importats. Lo tractament pòt trigar una estona.',
follow_import_error: 'Error en important los seguidors'
follow_import_error: 'Error en important los seguidors',
delete_account: 'Suprimir lo compte',
delete_account_description: 'Suprimir vòstre compte e los messatges per sempre.',
delete_account_instructions: 'Picatz vòstre senhal dins lo camp tèxte çai-jos per confirmar la supression del compte.',
delete_account_error: 'Una error ses producha en suprimir lo compte. Saquò ten darribar mercés de contactar vòstre administrador dinstància.',
follow_export: 'Exportar los abonaments',
follow_export_processing: 'Tractament, vos demandarem lèu de telecargar lo fichièr',
follow_export_button: 'Exportar vòstres abonaments dins un fichièr csv',
change_password: 'Cambiar lo senhal',
current_password: 'Senhal actual',
new_password: 'Nòu senhal',
confirm_new_password: 'Confirmatz lo nòu senhal',
changed_password: 'Senhal corrèctament cambiat',
change_password_error: 'Una error ses producha en cambiant lo senhal.'
},
notifications: {
notifications: 'Notficacions',
read: 'Legit!',
followed_you: 'vos sèc',
read: 'Legit!',
followed_you: 'vos a seguit',
favorited_you: 'a aimat vòstre estatut',
repeated_you: 'a repetit your vòstre estatut'
},
@ -1193,6 +1283,7 @@ const oc = {
},
post_status: {
posting: 'Mandadís',
content_warning: 'Avís de contengut (opcional)',
default: 'Escrivètz aquí vòstre estatut.'
},
finder: {
@ -1448,7 +1539,7 @@ const pt = {
title: 'Chat'
},
nav: {
chat: 'Chat Local',
chat: 'Chat local',
timeline: 'Linha do tempo',
mentions: 'Menções',
public_tl: 'Linha do tempo pública',
@ -1492,16 +1583,28 @@ const pt = {
theme: 'Tema',
presets: 'Predefinições',
theme_help: 'Use cores em código hexadecimal (#rrggbb) para personalizar seu esquema de cores.',
radii_help: 'Arredondar arestas da interface (em píxeis)',
background: 'Plano de Fundo',
foreground: 'Primeiro Plano',
text: 'Texto',
links: 'Links',
cBlue: 'Azul (Responder, seguir)',
cRed: 'Vermelho (Cancelar)',
cOrange: 'Laranja (Favoritar)',
cGreen: 'Verde (Repetir)',
btnRadius: 'Botões',
panelRadius: 'Paineis',
avatarRadius: 'Avatares',
avatarAltRadius: 'Avatares (Notificações)',
tooltipRadius: 'Dicass/alertas',
attachmentRadius: 'Anexos',
filtering: 'Filtragem',
filtering_explanation: 'Todas as postagens contendo estas palavras serão silenciadas, uma por linha.',
attachments: 'Anexos',
hide_attachments_in_tl: 'Ocultar anexos na linha do tempo.',
hide_attachments_in_convo: 'Ocultar anexos em conversas',
nsfw_clickthrough: 'Habilitar clique para ocultar anexos NSFW',
stop_gifs: 'Reproduzir GIFs ao passar o cursor em cima',
autoload: 'Habilitar carregamento automático quando a rolagem chegar ao fim.',
streaming: 'Habilitar o fluxo automático de postagens quando ao topo da página',
reply_link_preview: 'Habilitar a pré-visualização de link de respostas ao passar o mouse.',
@ -1512,8 +1615,10 @@ const pt = {
},
notifications: {
notifications: 'Notificações',
read: 'Ler!',
followed_you: 'seguiu você'
read: 'Lido!',
followed_you: 'seguiu você',
favorited_you: 'favoritou sua postagem',
repeated_you: 'repetiu sua postagem'
},
login: {
login: 'Entrar',
@ -1532,7 +1637,7 @@ const pt = {
},
post_status: {
posting: 'Publicando',
default: 'Acabo de aterrizar em L.A.'
default: 'Acabei de chegar no Rio!'
},
finder: {
find_user: 'Buscar usuário',
@ -1541,6 +1646,9 @@ const pt = {
general: {
submit: 'Enviar',
apply: 'Aplicar'
},
user_profile: {
timeline_title: 'Linha do tempo do usuário'
}
}
@ -1576,9 +1684,11 @@ const ru = {
load_older: 'Загрузить старые статусы',
conversation: 'Разговор',
collapse: 'Свернуть',
repeated: 'повторил(а)'
repeated: 'повторил(а)',
no_retweet_hint: 'Пост помечен как "только для подписчиков" или "личное" и поэтому не может быть повторён'
},
settings: {
general: 'Общие',
user_settings: 'Настройки пользователя',
name_bio: 'Имя и описание',
name: 'Имя',
@ -1593,9 +1703,11 @@ const ru = {
set_new_profile_background: 'Загрузить новый фон профиля',
settings: 'Настройки',
theme: 'Тема',
export_theme: 'Сохранить Тему',
import_theme: 'Загрузить Тему',
presets: 'Пресеты',
theme_help: 'Используйте шестнадцатеричные коды цветов (#rrggbb) для настройки темы.',
radii_help: 'Округление краёв элементов интерфейса (в пикселях)',
radii_help: 'Скругление углов элементов интерфейса (в пикселях)',
background: 'Фон',
foreground: 'Передний план',
text: 'Текст',
@ -1624,6 +1736,15 @@ const ru = {
loop_video: 'Зациливать видео',
loop_video_silent_only: 'Зацикливать только беззвучные видео (т.е. "гифки" с Mastodon)',
reply_link_preview: 'Включить предварительный просмотр ответа при наведении мыши',
replies_in_timeline: 'Ответы в ленте',
reply_visibility_all: 'Показывать все ответы',
reply_visibility_following: 'Показывать только ответы мне и тех на кого я подписан',
reply_visibility_self: 'Показывать только ответы мне',
notification_visibility: 'Показывать уведомления',
notification_visibility_likes: 'Лайки',
notification_visibility_mentions: 'Упоминания',
notification_visibility_repeats: 'Повторы',
notification_visibility_follows: 'Подписки',
follow_import: 'Импортировать читаемых',
import_followers_from_a_csv_file: 'Импортировать читаемых из файла .csv',
follows_imported: 'Список читаемых импортирован. Обработка займёт некоторое время..',
@ -1641,14 +1762,22 @@ const ru = {
confirm_new_password: 'Подтверждение нового пароля',
changed_password: 'Пароль изменён успешно.',
change_password_error: 'Произошла ошибка при попытке изменить пароль.',
limited_availability: 'Не доступно в вашем браузере'
lock_account_description: 'Аккаунт доступен только подтверждённым подписчикам',
limited_availability: 'Не доступно в вашем браузере',
profile_tab: 'Профиль',
security_tab: 'Безопасность',
data_import_export_tab: 'Импорт / Экспорт данных',
collapse_subject: 'Сворачивать посты с темой',
interfaceLanguage: 'Язык интерфейса'
},
notifications: {
notifications: 'Уведомления',
read: 'Прочесть',
followed_you: 'начал(а) читать вас',
favorited_you: 'нравится ваш статус',
repeated_you: 'повторил(а) ваш статус'
repeated_you: 'повторил(а) ваш статус',
broken_favorite: 'Неизвестный статус, ищем...',
load_older: 'Загрузить старые уведомления'
},
login: {
login: 'Войти',
@ -1668,7 +1797,18 @@ const ru = {
},
post_status: {
posting: 'Отправляется',
default: 'Что нового?'
content_warning: 'Тема (не обязательно)',
default: 'Что нового?',
account_not_locked_warning: 'Ваш аккаунт не {0}. Кто угодно может зафоловить вас чтобы прочитать посты только для подписчиков',
account_not_locked_warning_link: 'залочен',
direct_warning: 'Этот пост будет видет только упомянутым пользователям',
attachments_sensitive: 'Вложения содержат чувствительный контент',
scope: {
public: 'Публичный - этот пост виден всем',
unlisted: 'Непубличный - этот пост не виден на публичных лентах',
private: 'Для подписчиков - этот пост видят только подписчики',
direct: 'Личное - этот пост видят только те кто в нём упомянут'
}
},
finder: {
find_user: 'Найти пользователя',
@ -1806,8 +1946,18 @@ const he = {
chat: {
title: 'צ\'אט'
},
features_panel: {
chat: 'צ\'אט',
gopher: 'גופר',
media_proxy: 'מדיה פרוקסי',
scope_options: 'אפשרויות טווח',
text_limit: 'מגבלת טקסט',
title: 'מאפיינים',
who_to_follow: 'אחרי מי לעקוב'
},
nav: {
chat: 'צ\'אט מקומי',
friend_requests: 'בקשות עקיבה',
timeline: 'ציר הזמן',
mentions: 'אזכורים',
public_tl: 'ציר הזמן הציבורי',
@ -1825,7 +1975,9 @@ const he = {
followers: 'עוקבים',
followees: 'נעקבים',
per_day: 'ליום',
remote_follow: 'עקיבה מרחוק'
remote_follow: 'עקיבה מרחוק',
approve: 'אשר',
deny: 'דחה'
},
timeline: {
show_new: 'הראה חדש',
@ -1834,7 +1986,8 @@ const he = {
load_older: 'טען סטטוסים חדשים',
conversation: 'שיחה',
collapse: 'מוטט',
repeated: 'חזר'
repeated: 'חזר',
no_retweet_hint: 'ההודעה מסומנת כ"לעוקבים-בלבד" ולא ניתן לחזור עליה'
},
settings: {
user_settings: 'הגדרות משתמש',
@ -1852,7 +2005,10 @@ const he = {
settings: 'הגדרות',
theme: 'תמה',
presets: 'ערכים קבועים מראש',
export_theme: 'שמור ערכים',
import_theme: 'טען ערכים',
theme_help: 'השתמש בקודי צבע הקס (#אדום-אדום-ירוק-ירוק-כחול-כחול) על מנת להתאים אישית את תמת הצבע שלך.',
invalid_theme_imported: 'הקובץ הנבחר אינו תמה הנתמכת ע"י פלרומה. שום שינויים לא נעשו לתמה שלך.',
radii_help: 'קבע מראש עיגול פינות לממשק (בפיקסלים)',
background: 'רקע',
foreground: 'חזית',
@ -1875,10 +2031,23 @@ const he = {
hide_attachments_in_tl: 'החבא צירופים בציר הזמן',
hide_attachments_in_convo: 'החבא צירופים בשיחות',
nsfw_clickthrough: 'החל החבאת צירופים לא בטוחים לצפיה בעת עבודה בעזרת לחיצת עכבר',
collapse_subject: 'מזער הודעות עם נושאים',
stop_gifs: 'נגן-בעת-ריחוף GIFs',
autoload: 'החל טעינה אוטומטית בגלילה לתחתית הדף',
streaming: 'החל זרימת הודעות אוטומטית בעת גלילה למעלה הדף',
pause_on_unfocused: 'השהה זרימת הודעות כשהחלון לא בפוקוס',
loop_video: 'נגן סרטונים ללא הפסקה',
loop_video_silent_only: 'נגן רק סרטונים חסרי קול ללא הפסקה',
reply_link_preview: 'החל תצוגה מקדימה של לינק-תגובה בעת ריחוף עם העכבר',
replies_in_timeline: 'תגובות בציר הזמן',
reply_visibility_all: 'הראה את כל התגובות',
reply_visibility_following: 'הראה תגובות שמופנות אליי או לעקובים שלי בלבד',
reply_visibility_self: 'הראה תגובות שמופנות אליי בלבד',
notification_visibility: 'סוג ההתראות שתרצו לראות',
notification_visibility_likes: 'לייקים',
notification_visibility_mentions: 'אזכורים',
notification_visibility_repeats: 'חזרות',
notification_visibility_follows: 'עקיבות',
follow_import: 'יבוא עקיבות',
import_followers_from_a_csv_file: 'ייבא את הנעקבים שלך מקובץ csv',
follows_imported: 'נעקבים יובאו! ייקח זמן מה לעבד אותם.',
@ -1895,9 +2064,18 @@ const he = {
new_password: 'סיסמה חדשה',
confirm_new_password: 'אשר סיסמה',
changed_password: 'סיסמה שונתה בהצלחה!',
change_password_error: 'הייתה בעיה בשינוי סיסמתך.'
change_password_error: 'הייתה בעיה בשינוי סיסמתך.',
lock_account_description: 'הגבל את המשתמש לעוקבים מאושרים בלבד',
limited_availability: 'לא זמין בדפדפן שלך',
default_vis: 'ברירת מחדל לטווח הנראות',
profile_tab: 'פרופיל',
security_tab: 'ביטחון',
data_import_export_tab: 'ייבוא או ייצוא מידע',
interfaceLanguage: 'שפת הממשק'
},
notifications: {
broken_favorite: 'סטאטוס לא ידוע, מחפש...',
load_older: 'טען התראות ישנות',
notifications: 'התראות',
read: 'קרא!',
followed_you: 'עקב אחריך!',
@ -1917,9 +2095,24 @@ const he = {
fullname: 'שם תצוגה',
email: 'אימייל',
bio: 'אודות',
password_confirm: 'אישור סיסמה'
password_confirm: 'אישור סיסמה',
token: 'טוקן הזמנה'
},
post_status: {
account_not_locked_warning: 'המשתמש שלך אינו {0}. כל אחד יכול לעקוב אחריך ולראות את ההודעות לעוקבים-בלבד שלך.',
account_not_locked_warning_link: 'נעול',
attachments_sensitive: 'סמן מסמכים מצורפים כלא בטוחים לצפייה',
content_type: {
plain_text: 'טקסט פשוט'
},
content_warning: 'נושא (נתון לבחירה)',
direct_warning: 'הודעה זו תהיה זמינה רק לאנשים המוזכרים.',
scope: {
direct: 'ישיר - שלח לאנשים המוזכרים בלבד',
private: 'עוקבים-בלבד - שלח לעוקבים בלבד',
public: 'ציבורי - שלח לציר הזמן הציבורי',
unlisted: 'מחוץ לרשימה - אל תשלח לציר הזמן הציבורי'
},
posting: 'מפרסם',
default: 'הרגע נחת ב-ל.א.'
},
@ -1933,6 +2126,10 @@ const he = {
},
user_profile: {
timeline_title: 'ציר זמן המשתמש'
},
who_to_follow: {
who_to_follow: 'אחרי מי לעקוב',
more: 'עוד'
}
}

View File

@ -49,6 +49,8 @@ const persistedStateOptions = {
'config.hideAttachments',
'config.hideAttachmentsInConv',
'config.hideNsfw',
'config.replyVisibility',
'config.notificationVisibility',
'config.autoLoad',
'config.hoverPreview',
'config.streaming',
@ -59,7 +61,9 @@ const persistedStateOptions = {
'config.loopVideoSilentOnly',
'config.pauseOnUnfocused',
'config.stopGifs',
'users.lastLoginName'
'config.interfaceLanguage',
'users.lastLoginName',
'statuses.notifications.maxSavedId'
]
}
@ -77,6 +81,7 @@ const store = new Vuex.Store({
})
const i18n = new VueI18n({
// By default, use the browser locale, we will update it if neccessary
locale: currentLocale,
fallbackLocale: 'en',
messages
@ -91,65 +96,81 @@ window.fetch('/api/statusnet/config.json')
store.dispatch('setOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
store.dispatch('setOption', { name: 'textlimit', value: parseInt(textlimit) })
store.dispatch('setOption', { name: 'server', value: server })
})
window.fetch('/static/config.json')
.then((res) => res.json())
.then((data) => {
const {theme, background, logo, showWhoToFollowPanel, whoToFollowProvider, whoToFollowLink, showInstanceSpecificPanel, scopeOptionsEnabled, collapseMessageWithSubject} = data
store.dispatch('setOption', { name: 'theme', value: theme })
store.dispatch('setOption', { name: 'background', value: background })
store.dispatch('setOption', { name: 'logo', value: logo })
store.dispatch('setOption', { name: 'showWhoToFollowPanel', value: showWhoToFollowPanel })
store.dispatch('setOption', { name: 'whoToFollowProvider', value: whoToFollowProvider })
store.dispatch('setOption', { name: 'whoToFollowLink', value: whoToFollowLink })
store.dispatch('setOption', { name: 'showInstanceSpecificPanel', value: showInstanceSpecificPanel })
store.dispatch('setOption', { name: 'scopeOptionsEnabled', value: scopeOptionsEnabled })
store.dispatch('setOption', { name: 'collapseMessageWithSubject', value: collapseMessageWithSubject })
if (data['chatDisabled']) {
store.dispatch('disableChat')
}
var apiConfig = data.site.pleromafe
const routes = [
{ name: 'root',
path: '/',
redirect: to => {
var redirectRootLogin = data['redirectRootLogin']
var redirectRootNoLogin = data['redirectRootNoLogin']
return (store.state.users.currentUser ? redirectRootLogin : redirectRootNoLogin) || '/main/all'
}},
{ path: '/main/all', component: PublicAndExternalTimeline },
{ path: '/main/public', component: PublicTimeline },
{ path: '/main/friends', component: FriendsTimeline },
{ path: '/tag/:tag', component: TagTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
{ name: 'user-profile', path: '/users/:id', component: UserProfile },
{ name: 'mentions', path: '/:username/mentions', component: Mentions },
{ name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration },
{ name: 'registration', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
{ name: 'user-settings', path: '/user-settings', component: UserSettings }
]
window.fetch('/static/config.json')
.then((res) => res.json())
.then((data) => {
var staticConfig = data
// This takes static config and overrides properties that are present in apiConfig
var config = Object.assign({}, staticConfig, apiConfig)
const router = new VueRouter({
mode: 'history',
routes,
scrollBehavior: (to, from, savedPosition) => {
if (to.matched.some(m => m.meta.dontScroll)) {
return false
}
return savedPosition || { x: 0, y: 0 }
var theme = (config.theme)
var background = (config.background)
var logo = (config.logo)
var logoMask = (typeof config.logoMask === 'undefined' ? true : config.logoMask)
var logoMargin = (typeof config.logoMargin === 'undefined' ? 0 : config.logoMargin)
var redirectRootNoLogin = (config.redirectRootNoLogin)
var redirectRootLogin = (config.redirectRootLogin)
var chatDisabled = (config.chatDisabled)
var showInstanceSpecificPanel = (config.showInstanceSpecificPanel)
var scopeOptionsEnabled = (config.scopeOptionsEnabled)
var formattingOptionsEnabled = (config.formattingOptionsEnabled)
var defaultCollapseMessageWithSubject = (config.collapseMessageWithSubject)
store.dispatch('setOption', { name: 'theme', value: theme })
store.dispatch('setOption', { name: 'background', value: background })
store.dispatch('setOption', { name: 'logo', value: logo })
store.dispatch('setOption', { name: 'logoMask', value: logoMask })
store.dispatch('setOption', { name: 'logoMargin', value: logoMargin })
store.dispatch('setOption', { name: 'showInstanceSpecificPanel', value: showInstanceSpecificPanel })
store.dispatch('setOption', { name: 'scopeOptionsEnabled', value: scopeOptionsEnabled })
store.dispatch('setOption', { name: 'formattingOptionsEnabled', value: formattingOptionsEnabled })
store.dispatch('setOption', { name: 'defaultCollapseMessageWithSubject', value: defaultCollapseMessageWithSubject })
if (chatDisabled) {
store.dispatch('disableChat')
}
})
/* eslint-disable no-new */
new Vue({
router,
store,
i18n,
el: '#app',
render: h => h(App)
const routes = [
{ name: 'root',
path: '/',
redirect: to => {
return (store.state.users.currentUser ? redirectRootLogin : redirectRootNoLogin) || '/main/all'
}},
{ path: '/main/all', component: PublicAndExternalTimeline },
{ path: '/main/public', component: PublicTimeline },
{ path: '/main/friends', component: FriendsTimeline },
{ path: '/tag/:tag', component: TagTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
{ name: 'user-profile', path: '/users/:id', component: UserProfile },
{ name: 'mentions', path: '/:username/mentions', component: Mentions },
{ name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration },
{ name: 'registration', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
{ name: 'user-settings', path: '/user-settings', component: UserSettings }
]
const router = new VueRouter({
mode: 'history',
routes,
scrollBehavior: (to, from, savedPosition) => {
if (to.matched.some(m => m.meta.dontScroll)) {
return false
}
return savedPosition || { x: 0, y: 0 }
}
})
/* eslint-disable no-new */
new Vue({
router,
store,
i18n,
el: '#app',
render: h => h(App)
})
})
})
@ -192,3 +213,15 @@ window.fetch('/instance/panel.html')
store.dispatch('setOption', { name: 'instanceSpecificPanelContent', value: html })
})
window.fetch('/nodeinfo/2.0.json')
.then((res) => res.json())
.then((data) => {
const metadata = data.metadata
store.dispatch('setOption', { name: 'mediaProxyAvailable', value: data.metadata.mediaProxy })
store.dispatch('setOption', { name: 'chatAvailable', value: data.metadata.chat })
store.dispatch('setOption', { name: 'gopherAvailable', value: data.metadata.gopher })
const suggestions = metadata.suggestions
store.dispatch('setOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
store.dispatch('setOption', { name: 'suggestionsWeb', value: suggestions.web })
})

View File

@ -46,6 +46,9 @@ const api = {
store.commit('addFetcher', {timeline, fetcher})
}
},
fetchOldPost (store, { postId }) {
store.state.backendInteractor.fetchOldPost({ store, postId })
},
stopFetching (store, timeline) {
const fetcher = store.state.fetchers[timeline]
window.clearInterval(fetcher)

View File

@ -1,6 +1,8 @@
import { set, delete as del } from 'vue'
import StyleSetter from '../services/style_setter/style_setter.js'
const browserLocale = (window.navigator.language || 'en').split('-')[0]
const defaultState = {
name: 'Pleroma FE',
colors: {},
@ -15,8 +17,16 @@ const defaultState = {
hoverPreview: true,
pauseOnUnfocused: true,
stopGifs: false,
replyVisibility: 'all',
notificationVisibility: {
follows: true,
mentions: true,
likes: true,
repeats: true
},
muteWords: [],
highlight: {}
highlight: {},
interfaceLanguage: browserLocale
}
const config = {

View File

@ -1,4 +1,5 @@
import { includes, remove, slice, sortBy, toInteger, each, find, flatten, maxBy, minBy, merge, last, isArray } from 'lodash'
import { set } from 'vue'
import apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.js'
@ -22,13 +23,22 @@ export const defaultState = {
allStatuses: [],
allStatusesObject: {},
maxId: 0,
notifications: [],
notifications: {
desktopNotificationSilence: true,
maxId: 0,
maxSavedId: 0,
minId: Number.POSITIVE_INFINITY,
data: [],
error: false,
brokenFavorites: {}
},
favorites: new Set(),
error: false,
timelines: {
mentions: emptyTl(),
public: emptyTl(),
user: emptyTl(),
own: emptyTl(),
publicAndExternal: emptyTl(),
friends: emptyTl(),
tag: emptyTl()
@ -58,6 +68,15 @@ export const prepareStatus = (status) => {
return status
}
const visibleNotificationTypes = (rootState) => {
return [
rootState.config.notificationVisibility.likes && 'like',
rootState.config.notificationVisibility.mentions && 'mention',
rootState.config.notificationVisibility.repeats && 'repeat',
rootState.config.notificationVisibility.follows && 'follow'
].filter(_ => _)
}
export const statusType = (status) => {
if (status.is_post_verb) {
return 'status'
@ -76,8 +95,7 @@ export const statusType = (status) => {
return 'deletion'
}
// TODO change to status.activity_type === 'follow' when gs supports it
if (status.text.match(/started following/)) {
if (status.text.match(/started following/) || status.activity_type === 'follow') {
return 'follow'
}
@ -134,11 +152,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
const result = mergeOrAdd(allStatuses, allStatusesObject, status)
status = result.item
if (result.new) {
if (statusType(status) === 'retweet' && status.retweeted_status.user.id === user.id) {
addNotification({ type: 'repeat', status: status, action: status })
}
const brokenFavorites = state.notifications.brokenFavorites[status.id] || []
brokenFavorites.forEach((fav) => {
fav.status = status
})
delete state.notifications.brokenFavorites[status.id]
if (result.new) {
// We are mentioned in a post
if (statusType(status) === 'status' && find(status.attentions, { id: user.id })) {
const mentions = state.timelines.mentions
@ -150,10 +170,6 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
sortTimeline(mentions)
}
// Don't add notification for self-mention
if (status.user.id !== user.id) {
addNotification({ type: 'mention', status, action: status })
}
}
}
@ -176,45 +192,14 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
return status
}
const addNotification = ({type, status, action}) => {
// Only add a new notification if we don't have one for the same action
if (!find(state.notifications, (oldNotification) => oldNotification.action.id === action.id)) {
state.notifications.push({ type, status, action, seen: false })
if ('Notification' in window && window.Notification.permission === 'granted') {
const title = action.user.name
const result = {}
result.icon = action.user.profile_image_url
result.body = action.text // there's a problem that it doesn't put a space before links tho
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
if (action.attachments && action.attachments.length > 0 && !action.nsfw &&
action.attachments[0].mimetype.startsWith('image/')) {
result.image = action.attachments[0].url
}
let notification = new window.Notification(title, result)
// Chrome is known for not closing notifications automatically
// according to MDN, anyway.
setTimeout(notification.close.bind(notification), 5000)
}
}
}
const favoriteStatus = (favorite) => {
const favoriteStatus = (favorite, counter) => {
const status = find(allStatuses, { id: toInteger(favorite.in_reply_to_status_id) })
if (status) {
status.fave_num += 1
// This is our favorite, so the relevant bit.
if (favorite.user.id === user.id) {
status.favorited = true
}
// Add a notification if the user's status is favorited
if (status.user.id === user.id) {
addNotification({type: 'favorite', status, action: favorite})
} else {
status.fave_num += 1
}
}
return status
@ -248,18 +233,12 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
},
'favorite': (favorite) => {
// Only update if this is a new favorite.
// Ignore our own favorites because we get info about likes as response to like request
if (!state.favorites.has(favorite.id)) {
state.favorites.add(favorite.id)
favoriteStatus(favorite)
}
},
'follow': (status) => {
let re = new RegExp(`started following ${user.name} \\(${user.statusnet_profile_url}\\)`)
let repleroma = new RegExp(`started following ${user.screen_name}$`)
if (status.text.match(re) || status.text.match(repleroma)) {
addNotification({ type: 'follow', status: status, action: status })
}
},
'deletion': (deletion) => {
const uri = deletion.uri
@ -269,7 +248,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
return
}
remove(state.notifications, ({action: {id}}) => id === status.id)
remove(state.notifications.data, ({action: {id}}) => id === status.id)
remove(allStatuses, { uri })
if (timeline) {
@ -298,8 +277,69 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
}
}
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes }) => {
const allStatuses = state.allStatuses
const allStatusesObject = state.allStatusesObject
each(notifications, (notification) => {
const result = mergeOrAdd(allStatuses, allStatusesObject, notification.notice)
const action = result.item
// Only add a new notification if we don't have one for the same action
if (!find(state.notifications.data, (oldNotification) => oldNotification.action.id === action.id)) {
state.notifications.maxId = Math.max(notification.id, state.notifications.maxId)
state.notifications.minId = Math.min(notification.id, state.notifications.minId)
const fresh = !older && !notification.is_seen && notification.id > state.notifications.maxSavedId
const status = notification.ntype === 'like'
? find(allStatuses, { id: action.in_reply_to_status_id })
: action
const result = {
type: notification.ntype,
status,
action,
// Always assume older notifications as seen
seen: !fresh
}
if (notification.ntype === 'like' && !status) {
let broken = state.notifications.brokenFavorites[action.in_reply_to_status_id]
if (broken) {
broken.push(result)
} else {
dispatch('fetchOldPost', { postId: action.in_reply_to_status_id })
broken = [ result ]
state.notifications.brokenFavorites[action.in_reply_to_status_id] = broken
}
}
state.notifications.data.push(result)
if ('Notification' in window && window.Notification.permission === 'granted') {
const title = action.user.name
const result = {}
result.icon = action.user.profile_image_url
result.body = action.text // there's a problem that it doesn't put a space before links tho
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
if (action.attachments && action.attachments.length > 0 && !action.nsfw &&
action.attachments[0].mimetype.startsWith('image/')) {
result.image = action.attachments[0].url
}
if (fresh && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.ntype)) {
let notification = new window.Notification(title, result)
// Chrome is known for not closing notifications automatically
// according to MDN, anyway.
setTimeout(notification.close.bind(notification), 5000)
}
}
}
})
}
export const mutations = {
addNewStatuses,
addNewNotifications,
showNewStatuses (state, { timeline }) {
const oldTimeline = (state.timelines[timeline])
@ -316,6 +356,11 @@ export const mutations = {
const newStatus = state.allStatusesObject[status.id]
newStatus.favorited = value
},
setFavoritedConfirm (state, { status }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.favorited = status.favorited
newStatus.fave_num = status.fave_num
},
setRetweeted (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.repeated = value
@ -334,6 +379,12 @@ export const mutations = {
setError (state, { value }) {
state.error = value
},
setNotificationsError (state, { value }) {
state.notifications.error = value
},
setNotificationsSilence (state, { value }) {
state.notifications.desktopNotificationSilence = value
},
setProfileView (state, { v }) {
// load followers / friends only when needed
state.timelines['user'].viewing = v
@ -345,6 +396,7 @@ export const mutations = {
state.timelines['user'].followers = followers
},
markNotificationsAsSeen (state, notifications) {
set(state.notifications, 'maxSavedId', state.notifications.maxId)
each(notifications, (notification) => {
notification.seen = true
})
@ -360,9 +412,18 @@ const statuses = {
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false }) {
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser })
},
addNewNotifications ({ rootState, commit, dispatch }, { notifications, older }) {
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older })
},
setError ({ rootState, commit }, { value }) {
commit('setError', { value })
},
setNotificationsError ({ rootState, commit }, { value }) {
commit('setNotificationsError', { value })
},
setNotificationsSilence ({ rootState, commit }, { value }) {
commit('setNotificationsSilence', { value })
},
addFriends ({ rootState, commit }, { friends }) {
commit('addFriends', { friends })
},
@ -377,11 +438,31 @@ const statuses = {
// Optimistic favoriting...
commit('setFavorited', { status, value: true })
apiService.favorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
.then(response => {
if (response.ok) {
return response.json()
} else {
return {}
}
})
.then(status => {
commit('setFavoritedConfirm', { status })
})
},
unfavorite ({ rootState, commit }, status) {
// Optimistic favoriting...
commit('setFavorited', { status, value: false })
apiService.unfavorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
.then(response => {
if (response.ok) {
return response.json()
} else {
return {}
}
})
.then(status => {
commit('setFavoritedConfirm', { status })
})
},
retweet ({ rootState, commit }, status) {
// Optimistic retweeting...

View File

@ -107,6 +107,8 @@ const users = {
// Start getting fresh tweets.
store.dispatch('startFetching', 'friends')
// Start getting our own posts, only really needed for mitigating broken favorites
store.dispatch('startFetching', ['own', user.id])
// Get user mutes and follower info
store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
@ -119,7 +121,7 @@ const users = {
}
// Fetch our friends
store.rootState.api.backendInteractor.fetchFriends()
store.rootState.api.backendInteractor.fetchFriends({id: user.id})
.then((friends) => commit('addNewUsers', friends))
})
} else {

View File

@ -27,6 +27,7 @@ const BANNER_UPDATE_URL = '/api/account/update_profile_banner.json'
const PROFILE_UPDATE_URL = '/api/account/update_profile.json'
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
const QVITTER_USER_TIMELINE_URL = '/api/qvitter/statuses/user_timeline.json'
const QVITTER_USER_NOTIFICATIONS_URL = '/api/qvitter/statuses/notifications.json'
const BLOCKING_URL = '/api/blocks/create.json'
const UNBLOCKING_URL = '/api/blocks/destroy.json'
const USER_URL = '/api/users/show.json'
@ -36,6 +37,7 @@ const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
const FOLLOW_REQUESTS_URL = '/api/pleroma/friend_requests'
const APPROVE_USER_URL = '/api/pleroma/friendships/approve'
const DENY_USER_URL = '/api/pleroma/friendships/deny'
const SUGGESTIONS_URL = '/api/v1/suggestions'
import { each, map } from 'lodash'
import 'whatwg-fetch'
@ -302,8 +304,12 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
public: PUBLIC_TIMELINE_URL,
friends: FRIENDS_TIMELINE_URL,
mentions: MENTIONS_URL,
notifications: QVITTER_USER_NOTIFICATIONS_URL,
'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL,
user: QVITTER_USER_TIMELINE_URL,
// separate timeline for own posts, so it won't break due to user timeline bugs
// really needed only for broken favorites
own: QVITTER_USER_TIMELINE_URL,
tag: TAG_TIMELINE_URL
}
@ -367,7 +373,7 @@ const unretweet = ({ id, credentials }) => {
})
}
const postStatus = ({credentials, status, spoilerText, visibility, mediaIds, inReplyToStatusId}) => {
const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType}) => {
const idsText = mediaIds.join(',')
const form = new FormData()
@ -375,6 +381,8 @@ const postStatus = ({credentials, status, spoilerText, visibility, mediaIds, inR
form.append('source', 'Pleroma FE')
if (spoilerText) form.append('spoiler_text', spoilerText)
if (visibility) form.append('visibility', visibility)
if (sensitive) form.append('sensitive', sensitive)
if (contentType) form.append('content_type', contentType)
form.append('media_ids', idsText)
if (inReplyToStatusId) {
form.append('in_reply_to_status_id', inReplyToStatusId)
@ -449,6 +457,12 @@ const fetchMutes = ({credentials}) => {
}).then((data) => data.json())
}
const suggestions = ({credentials}) => {
return fetch(SUGGESTIONS_URL, {
headers: authHeaders(credentials)
}).then((data) => data.json())
}
const apiService = {
verifyCredentials,
fetchTimeline,
@ -482,7 +496,8 @@ const apiService = {
changePassword,
fetchFollowRequests,
approveUser,
denyUser
denyUser,
suggestions
}
export default apiService

View File

@ -54,6 +54,16 @@ const backendInteractorService = (credentials) => {
return timelineFetcherService.startFetching({timeline, store, credentials, userId})
}
const fetchOldPost = ({store, postId}) => {
return timelineFetcherService.fetchAndUpdate({
store,
credentials,
timeline: 'own',
older: true,
until: postId + 1
})
}
const setUserMute = ({id, muted = true}) => {
return apiService.setUserMute({id, muted, credentials})
}
@ -86,6 +96,7 @@ const backendInteractorService = (credentials) => {
fetchAllFollowing,
verifyCredentials: apiService.verifyCredentials,
startFetching,
fetchOldPost,
setUserMute,
fetchMutes,
register,

View File

@ -0,0 +1,46 @@
import apiService from '../api/api.service.js'
const update = ({store, notifications, older}) => {
store.dispatch('setNotificationsError', { value: false })
store.dispatch('addNewNotifications', { notifications, older })
}
const fetchAndUpdate = ({store, credentials, older = false}) => {
const args = { credentials }
const rootState = store.rootState || store.state
const timelineData = rootState.statuses.notifications
if (older) {
if (timelineData.minId !== Number.POSITIVE_INFINITY) {
args['until'] = timelineData.minId
}
} else {
args['since'] = timelineData.maxId
}
args['timeline'] = 'notifications'
return apiService.fetchTimeline(args)
.then((notifications) => {
update({store, notifications, older})
}, () => store.dispatch('setNotificationsError', { value: true }))
.catch(() => store.dispatch('setNotificationsError', { value: true }))
}
const startFetching = ({credentials, store}) => {
fetchAndUpdate({ credentials, store })
const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store })
// Initially there's set flag to silence all desktop notifications so
// that there won't spam of them when user just opened up the FE we
// reset that flag after a while to show new notifications once again.
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
return setInterval(boundFetchAndUpdate, 10000)
}
const notificationsFetcher = {
fetchAndUpdate,
startFetching
}
export default notificationsFetcher

View File

@ -1,10 +1,10 @@
import { map } from 'lodash'
import apiService from '../api/api.service.js'
const postStatus = ({ store, status, spoilerText, visibility, media = [], inReplyToStatusId = undefined }) => {
const postStatus = ({ store, status, spoilerText, visibility, sensitive, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => {
const mediaIds = map(media, 'id')
return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, mediaIds, inReplyToStatusId})
return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType})
.then((data) => data.json())
.then((data) => {
if (!data.error) {

View File

@ -14,13 +14,13 @@ const update = ({store, statuses, timeline, showImmediately}) => {
})
}
const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false, userId = false, tag = false}) => {
const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false, userId = false, tag = false, until}) => {
const args = { timeline, credentials }
const rootState = store.rootState || store.state
const timelineData = rootState.statuses.timelines[camelCase(timeline)]
if (older) {
args['until'] = timelineData.minVisibleId
args['until'] = until || timelineData.minVisibleId
} else {
args['since'] = timelineData.maxId
}

View File

@ -2,15 +2,13 @@
"theme": "fairyfloss",
"background": "/static/windows_xp_bliss.jpg",
"logo": "/static/logo.png",
"logoMask": true,
"logoMargin": ".1em",
"redirectRootNoLogin": "/main/all",
"redirectRootLogin": "/main/friends",
"chatDisabled": false,
"showWhoToFollowPanel": false,
"whoToFollowProvider": "https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-osa-api.cgi?{{host}}+{{user}}",
"whoToFollowProviderDummy2": "https://followlink.osa-p.net/api/get_recommend.json?acct=@{{user}}@{{host}}",
"whoToFollowLink": "https://vinayaka.distsn.org/?{{host}}+{{user}}",
"whoToFollowLinkDummy2": "https://followlink.osa-p.net/recommend.html",
"chatDisabled": true,
"showInstanceSpecificPanel": true,
"scopeOptionsEnabled": true,
"formattingOptionsEnabled": true,
"collapseMessageWithSubject": true
}

View File

@ -247,7 +247,7 @@ describe('The Statuses module', () => {
in_reply_to_status_id: '1', // The API uses strings here...
uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
text: 'a favorited something by b',
user: {}
user: { id: 99 }
}
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
@ -264,7 +264,7 @@ describe('The Statuses module', () => {
expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
expect(state.timelines.public.maxId).to.eq(favorite.id)
// If something is favorited by the current user, it also sets the 'favorited' property
// If something is favorited by the current user, it also sets the 'favorited' property but does not increment counter to avoid over-counting. Counter is incremented (updated, really) via response to the favorite request.
const user = {
id: 1
}
@ -281,45 +281,11 @@ describe('The Statuses module', () => {
mutations.addNewStatuses(state, { statuses: [ownFavorite], showImmediately: true, timeline: 'public', user })
expect(state.timelines.public.visibleStatuses.length).to.eql(1)
expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(2)
expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
expect(state.timelines.public.visibleStatuses[0].favorited).to.eql(true)
})
describe('notifications', () => {
it('adds a notfications for retweets if you are the retweetet', () => {
const user = { id: 1 }
const state = cloneDeep(defaultState)
const status = makeMockStatus({id: 1})
status.user = user
const retweet = makeMockStatus({id: 2, is_post_verb: false})
retweet.retweeted_status = status
mutations.addNewStatuses(state, { statuses: [retweet], user })
expect(state.notifications.length).to.eql(1)
expect(state.notifications[0].status).to.eql(retweet)
expect(state.notifications[0].action).to.eql(retweet)
expect(state.notifications[0].type).to.eql('repeat')
})
it('adds a notification when you are mentioned', () => {
const user = { id: 1 }
const state = cloneDeep(defaultState)
const status = makeMockStatus({id: 1})
const mentionedStatus = makeMockStatus({id: 2})
mentionedStatus.attentions = [user]
mutations.addNewStatuses(state, { statuses: [status], user })
expect(state.notifications.length).to.eql(0)
mutations.addNewStatuses(state, { statuses: [mentionedStatus], user })
expect(state.notifications.length).to.eql(1)
expect(state.notifications[0].status).to.eql(mentionedStatus)
expect(state.notifications[0].action).to.eql(mentionedStatus)
expect(state.notifications[0].type).to.eql('mention')
})
it('removes a notification when the notice gets removed', () => {
const user = { id: 1 }
const state = cloneDeep(defaultState)
@ -335,92 +301,39 @@ describe('The Statuses module', () => {
deletion.uri = 'xxx'
mutations.addNewStatuses(state, { statuses: [status, otherStatus], user })
mutations.addNewNotifications(
state,
{
notifications: [{
ntype: 'mention',
status: otherStatus,
notice: otherStatus,
is_seen: false
}]
})
expect(state.notifications.length).to.eql(1)
expect(state.notifications.data.length).to.eql(1)
mutations.addNewNotifications(
state,
{
notifications: [{
ntype: 'mention',
status: mentionedStatus,
notice: mentionedStatus,
is_seen: false
}]
})
mutations.addNewStatuses(state, { statuses: [mentionedStatus], user })
expect(state.allStatuses.length).to.eql(3)
expect(state.notifications.length).to.eql(2)
expect(state.notifications[1].status).to.eql(mentionedStatus)
expect(state.notifications[1].action).to.eql(mentionedStatus)
expect(state.notifications[1].type).to.eql('mention')
expect(state.notifications.data.length).to.eql(2)
expect(state.notifications.data[1].status).to.eql(mentionedStatus)
expect(state.notifications.data[1].action).to.eql(mentionedStatus)
expect(state.notifications.data[1].type).to.eql('mention')
mutations.addNewStatuses(state, { statuses: [deletion], user })
expect(state.allStatuses.length).to.eql(2)
expect(state.notifications.length).to.eql(1)
})
it('adds the message to mentions when you are mentioned', () => {
const user = { id: 1 }
const state = cloneDeep(defaultState)
const status = makeMockStatus({id: 1})
const mentionedStatus = makeMockStatus({id: 2})
mentionedStatus.attentions = [user]
mutations.addNewStatuses(state, { statuses: [status], user })
expect(state.timelines.mentions.statuses).to.have.length(0)
mutations.addNewStatuses(state, { statuses: [mentionedStatus], user })
expect(state.timelines.mentions.statuses).to.have.length(1)
expect(state.timelines.mentions.statuses).to.eql([mentionedStatus])
})
it('adds a notfication when one of the user\'s status is favorited', () => {
const state = cloneDeep(defaultState)
const status = makeMockStatus({id: 1})
const user = {id: 1}
status.user = user
const favorite = {
id: 2,
is_post_verb: false,
in_reply_to_status_id: '1', // The API uses strings here...
uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
text: 'a favorited something by b',
user: {}
}
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public', user })
mutations.addNewStatuses(state, { statuses: [favorite], showImmediately: true, timeline: 'public', user })
expect(state.notifications).to.have.length(1)
})
it('adds a notification when the user is followed', () => {
const state = cloneDeep(defaultState)
const user = {id: 1, screen_name: 'b'}
const follower = {id: 2, screen_name: 'a'}
const follow = {
id: 3,
is_post_verb: false,
activity_type: 'follow',
text: 'a started following b',
user: follower
}
mutations.addNewStatuses(state, { statuses: [follow], showImmediately: true, timeline: 'public', user })
expect(state.notifications).to.have.length(1)
})
it('does not add a notification when an other user is followed', () => {
const state = cloneDeep(defaultState)
const user = {id: 1, screen_name: 'b'}
const follower = {id: 2, screen_name: 'a'}
const follow = {
id: 3,
is_post_verb: false,
activity_type: 'follow',
text: 'a started following b@shitposter.club',
user: follower
}
mutations.addNewStatuses(state, { statuses: [follow], showImmediately: true, timeline: 'public', user })
expect(state.notifications).to.have.length(0)
expect(state.notifications.data.length).to.eql(1)
})
})
})

115
yarn.lock
View File

@ -434,6 +434,10 @@ babel-helper-replace-supers@^6.24.1:
babel-traverse "^6.24.1"
babel-types "^6.24.1"
babel-helper-vue-jsx-merge-props@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz#22aebd3b33902328e513293a8e4992b384f9f1b6"
babel-helpers@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2"
@ -500,6 +504,10 @@ babel-plugin-syntax-exponentiation-operator@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
babel-plugin-syntax-jsx@^6.18.0:
version "6.18.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
babel-plugin-syntax-object-rest-spread@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
@ -516,7 +524,7 @@ babel-plugin-transform-async-generator-functions@^6.24.1:
babel-plugin-syntax-async-generators "^6.5.0"
babel-runtime "^6.22.0"
babel-plugin-transform-async-to-generator@^6.24.1:
babel-plugin-transform-async-to-generator@^6.22.0, babel-plugin-transform-async-to-generator@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761"
dependencies:
@ -555,7 +563,7 @@ babel-plugin-transform-es2015-block-scoped-functions@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
babel-plugin-transform-es2015-block-scoping@^6.24.1:
babel-plugin-transform-es2015-block-scoping@^6.23.0, babel-plugin-transform-es2015-block-scoping@^6.24.1:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f"
dependencies:
@ -565,7 +573,7 @@ babel-plugin-transform-es2015-block-scoping@^6.24.1:
babel-types "^6.26.0"
lodash "^4.17.4"
babel-plugin-transform-es2015-classes@^6.24.1:
babel-plugin-transform-es2015-classes@^6.23.0, babel-plugin-transform-es2015-classes@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db"
dependencies:
@ -579,33 +587,33 @@ babel-plugin-transform-es2015-classes@^6.24.1:
babel-traverse "^6.24.1"
babel-types "^6.24.1"
babel-plugin-transform-es2015-computed-properties@^6.24.1:
babel-plugin-transform-es2015-computed-properties@^6.22.0, babel-plugin-transform-es2015-computed-properties@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3"
dependencies:
babel-runtime "^6.22.0"
babel-template "^6.24.1"
babel-plugin-transform-es2015-destructuring@^6.22.0:
babel-plugin-transform-es2015-destructuring@^6.22.0, babel-plugin-transform-es2015-destructuring@^6.23.0:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d"
dependencies:
babel-runtime "^6.22.0"
babel-plugin-transform-es2015-duplicate-keys@^6.24.1:
babel-plugin-transform-es2015-duplicate-keys@^6.22.0, babel-plugin-transform-es2015-duplicate-keys@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e"
dependencies:
babel-runtime "^6.22.0"
babel-types "^6.24.1"
babel-plugin-transform-es2015-for-of@^6.22.0:
babel-plugin-transform-es2015-for-of@^6.22.0, babel-plugin-transform-es2015-for-of@^6.23.0:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691"
dependencies:
babel-runtime "^6.22.0"
babel-plugin-transform-es2015-function-name@^6.24.1:
babel-plugin-transform-es2015-function-name@^6.22.0, babel-plugin-transform-es2015-function-name@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b"
dependencies:
@ -619,7 +627,7 @@ babel-plugin-transform-es2015-literals@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
babel-plugin-transform-es2015-modules-amd@^6.24.1:
babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154"
dependencies:
@ -627,6 +635,15 @@ babel-plugin-transform-es2015-modules-amd@^6.24.1:
babel-runtime "^6.22.0"
babel-template "^6.24.1"
babel-plugin-transform-es2015-modules-commonjs@^6.23.0:
version "6.26.2"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3"
dependencies:
babel-plugin-transform-strict-mode "^6.24.1"
babel-runtime "^6.26.0"
babel-template "^6.26.0"
babel-types "^6.26.0"
babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a"
@ -636,7 +653,7 @@ babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
babel-template "^6.26.0"
babel-types "^6.26.0"
babel-plugin-transform-es2015-modules-systemjs@^6.24.1:
babel-plugin-transform-es2015-modules-systemjs@^6.23.0, babel-plugin-transform-es2015-modules-systemjs@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23"
dependencies:
@ -644,7 +661,7 @@ babel-plugin-transform-es2015-modules-systemjs@^6.24.1:
babel-runtime "^6.22.0"
babel-template "^6.24.1"
babel-plugin-transform-es2015-modules-umd@^6.24.1:
babel-plugin-transform-es2015-modules-umd@^6.23.0, babel-plugin-transform-es2015-modules-umd@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468"
dependencies:
@ -652,14 +669,14 @@ babel-plugin-transform-es2015-modules-umd@^6.24.1:
babel-runtime "^6.22.0"
babel-template "^6.24.1"
babel-plugin-transform-es2015-object-super@^6.24.1:
babel-plugin-transform-es2015-object-super@^6.22.0, babel-plugin-transform-es2015-object-super@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d"
dependencies:
babel-helper-replace-supers "^6.24.1"
babel-runtime "^6.22.0"
babel-plugin-transform-es2015-parameters@^6.24.1:
babel-plugin-transform-es2015-parameters@^6.23.0, babel-plugin-transform-es2015-parameters@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b"
dependencies:
@ -670,7 +687,7 @@ babel-plugin-transform-es2015-parameters@^6.24.1:
babel-traverse "^6.24.1"
babel-types "^6.24.1"
babel-plugin-transform-es2015-shorthand-properties@^6.24.1:
babel-plugin-transform-es2015-shorthand-properties@^6.22.0, babel-plugin-transform-es2015-shorthand-properties@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0"
dependencies:
@ -683,7 +700,7 @@ babel-plugin-transform-es2015-spread@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
babel-plugin-transform-es2015-sticky-regex@^6.24.1:
babel-plugin-transform-es2015-sticky-regex@^6.22.0, babel-plugin-transform-es2015-sticky-regex@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
dependencies:
@ -697,13 +714,13 @@ babel-plugin-transform-es2015-template-literals@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
babel-plugin-transform-es2015-typeof-symbol@^6.22.0:
babel-plugin-transform-es2015-typeof-symbol@^6.22.0, babel-plugin-transform-es2015-typeof-symbol@^6.23.0:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372"
dependencies:
babel-runtime "^6.22.0"
babel-plugin-transform-es2015-unicode-regex@^6.24.1:
babel-plugin-transform-es2015-unicode-regex@^6.22.0, babel-plugin-transform-es2015-unicode-regex@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9"
dependencies:
@ -711,7 +728,7 @@ babel-plugin-transform-es2015-unicode-regex@^6.24.1:
babel-runtime "^6.22.0"
regexpu-core "^2.0.0"
babel-plugin-transform-exponentiation-operator@^6.24.1:
babel-plugin-transform-exponentiation-operator@^6.22.0, babel-plugin-transform-exponentiation-operator@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e"
dependencies:
@ -726,7 +743,7 @@ babel-plugin-transform-object-rest-spread@^6.22.0:
babel-plugin-syntax-object-rest-spread "^6.8.0"
babel-runtime "^6.26.0"
babel-plugin-transform-regenerator@^6.24.1:
babel-plugin-transform-regenerator@^6.22.0, babel-plugin-transform-regenerator@^6.24.1:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"
dependencies:
@ -745,6 +762,47 @@ babel-plugin-transform-strict-mode@^6.24.1:
babel-runtime "^6.22.0"
babel-types "^6.24.1"
babel-plugin-transform-vue-jsx@3:
version "3.7.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-3.7.0.tgz#d40492e6692a36b594f7e9a1928f43e969740960"
dependencies:
esutils "^2.0.2"
babel-preset-env@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.7.0.tgz#dea79fa4ebeb883cd35dab07e260c1c9c04df77a"
dependencies:
babel-plugin-check-es2015-constants "^6.22.0"
babel-plugin-syntax-trailing-function-commas "^6.22.0"
babel-plugin-transform-async-to-generator "^6.22.0"
babel-plugin-transform-es2015-arrow-functions "^6.22.0"
babel-plugin-transform-es2015-block-scoped-functions "^6.22.0"
babel-plugin-transform-es2015-block-scoping "^6.23.0"
babel-plugin-transform-es2015-classes "^6.23.0"
babel-plugin-transform-es2015-computed-properties "^6.22.0"
babel-plugin-transform-es2015-destructuring "^6.23.0"
babel-plugin-transform-es2015-duplicate-keys "^6.22.0"
babel-plugin-transform-es2015-for-of "^6.23.0"
babel-plugin-transform-es2015-function-name "^6.22.0"
babel-plugin-transform-es2015-literals "^6.22.0"
babel-plugin-transform-es2015-modules-amd "^6.22.0"
babel-plugin-transform-es2015-modules-commonjs "^6.23.0"
babel-plugin-transform-es2015-modules-systemjs "^6.23.0"
babel-plugin-transform-es2015-modules-umd "^6.23.0"
babel-plugin-transform-es2015-object-super "^6.22.0"
babel-plugin-transform-es2015-parameters "^6.23.0"
babel-plugin-transform-es2015-shorthand-properties "^6.22.0"
babel-plugin-transform-es2015-spread "^6.22.0"
babel-plugin-transform-es2015-sticky-regex "^6.22.0"
babel-plugin-transform-es2015-template-literals "^6.22.0"
babel-plugin-transform-es2015-typeof-symbol "^6.23.0"
babel-plugin-transform-es2015-unicode-regex "^6.22.0"
babel-plugin-transform-exponentiation-operator "^6.22.0"
babel-plugin-transform-regenerator "^6.22.0"
browserslist "^3.2.6"
invariant "^2.2.2"
semver "^5.3.0"
babel-preset-es2015@^6.0.0:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939"
@ -996,6 +1054,13 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
caniuse-db "^1.0.30000639"
electron-to-chromium "^1.2.7"
browserslist@^3.2.6:
version "3.2.8"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6"
dependencies:
caniuse-lite "^1.0.30000844"
electron-to-chromium "^1.3.47"
buffer@^4.9.0:
version "4.9.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
@ -1069,6 +1134,10 @@ caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
version "1.0.30000801"
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000801.tgz#a1d49def94c4e5aca5ccf1d58812e4668fac19d4"
caniuse-lite@^1.0.30000844:
version "1.0.30000878"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000878.tgz#c644c39588dd42d3498e952234c372e5a40a4123"
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
@ -1789,6 +1858,10 @@ electron-to-chromium@^1.2.7:
version "1.3.32"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.32.tgz#11d0684c0840e003c4be8928f8ac5f35dbc2b4e6"
electron-to-chromium@^1.3.47:
version "1.3.61"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.61.tgz#a8ac295b28d0f03d85e37326fd16b6b6b17a1795"
emojis-list@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
@ -3081,6 +3154,10 @@ isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
iso-639-1@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/iso-639-1/-/iso-639-1-2.0.3.tgz#72dd3448ac5629c271628c5ac566369428d6ccd0"
isobject@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"