Merge branch 'develop' of git.pleroma.social:pleroma/pleroma-fe into develop
This commit is contained in:
commit
08444c3903
247 changed files with 12235 additions and 8148 deletions
63
CHANGELOG.md
63
CHANGELOG.md
|
@ -2,9 +2,61 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
### Changed
|
||||
- Greentext now has separate color slot for it
|
||||
- Removed the use of with_move parameters when fetching notifications
|
||||
|
||||
### Fixed
|
||||
- Weird bug related to post being sent seemingly after pasting with keyboard (hopefully)
|
||||
- Multiple issues with muted statuses/notifications
|
||||
|
||||
## [Unreleased patch]
|
||||
### Add
|
||||
- Added private notifications option for push notifications
|
||||
- 'Copy link' button for statuses (in the ellipsis menu)
|
||||
- Autocomplete domains from list of known instances
|
||||
|
||||
### Changed
|
||||
- Registration page no longer requires email if the server is configured not to require it
|
||||
- Change heart to thumbs up in reaction picker
|
||||
- Close the media modal on navigation events
|
||||
- Add colons to the emoji alt text, to make them copyable
|
||||
- Add better visual indication for drag-and-drop for files
|
||||
|
||||
### Fixed
|
||||
- Status ellipsis menu closes properly when selecting certain options
|
||||
- Cropped images look correct in Chrome
|
||||
- Newlines in the muted words settings work again
|
||||
- Clicking on non-latin hashtags won't open a new window
|
||||
- Uploading and drag-dropping multiple files works correctly now.
|
||||
- Subject field now appears disabled when posting
|
||||
|
||||
## [2.0.3] - 2020-05-02
|
||||
### Fixed
|
||||
- Show more/less works correctly with auto-collapsed subjects and long posts
|
||||
- RTL characters won't look messed up in notifications
|
||||
|
||||
### Changed
|
||||
- Emoji autocomplete will match any part of the word and not just start, for example :drool will now helpfully suggest :blobcatdrool: and :blobcatdroolreach:
|
||||
|
||||
### Add
|
||||
- Follow request notification support
|
||||
|
||||
## [2.0.2] - 2020-04-08
|
||||
### Fixed
|
||||
- Favorite/Repeat avatars not showing up on private instances/non-public posts
|
||||
- Autocorrect getting triggered in the captcha field
|
||||
- Overflow on long domains in follow/move notifications
|
||||
|
||||
### Changed
|
||||
- Polish translation updated
|
||||
|
||||
## [2.0.0] - 2020-02-28
|
||||
### Added
|
||||
- Tons of color slots including ones for hover/pressed/toggled buttons
|
||||
- Experimental `--variable[,mod]` syntax support for color slots in themes. the `mod` makes color brighter/darker depending on background color (makes darker color brighter/darker depending on background color)
|
||||
- Paper theme by Shpuld
|
||||
- Icons in nav panel
|
||||
- Private mode support
|
||||
- Support for 'Move' type notifications
|
||||
|
@ -13,14 +65,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Emoji reactions for statuses
|
||||
- MRF keyword policy disclosure
|
||||
### Changed
|
||||
- Updated Pleroma default themes
|
||||
- theme engine update to 3 (themes v2.1 introduction)
|
||||
- massive internal changes in theme engine - slowly away from "generate things separately with spaghetti code" towards "feed all data into single 'generateTheme' function and declare slot inheritance and all in a separate file"
|
||||
- Breezy theme updates to make it closer to actual Breeze in some aspects
|
||||
- when using `--variable` in shadows it no longer uses the actual CSS3 variable, instead it generates color from other slots
|
||||
- theme doesn't get saved to local storage when opening FE anonymously
|
||||
- Captcha now resets on failed registrations
|
||||
- Notifications column now cleans itself up to optimize performance when tab is left open for a long time
|
||||
- 403 messaging
|
||||
### Fixed
|
||||
- Fixed loader-spinner not disappearing when a status preview fails to load
|
||||
- anon viewers won't get theme data saved to local storage, so admin changing default theme will have an effect for users coming back to instance.
|
||||
- Single notifications left unread when hitting read on another device/tab
|
||||
- Registration fixed
|
||||
- Deactivation of remote accounts from frontend
|
||||
- Fixed NSFW unhiding not working with videos when using one-click unhiding/displaying
|
||||
- Improved performance of anything that uses popovers (most notably statuses)
|
||||
|
||||
## [1.1.7 and earlier] - 2019-12-14
|
||||
### Added
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# pleroma_fe
|
||||
# Pleroma-FE
|
||||
|
||||
> A single column frontend for both Pleroma and GS servers.
|
||||
> A single column frontend designed for Pleroma.
|
||||
|
||||
![screenshot](https://i.imgur.com/DJVqSJ0.png)
|
||||
|
||||
|
@ -11,7 +11,6 @@ To translate Pleroma-FE, add your language to [src/i18n/messages.js](https://git
|
|||
# FOR ADMINS
|
||||
|
||||
You don't need to build Pleroma-FE yourself. Those using the Pleroma backend will be able to use it out of the box.
|
||||
For the GNU social backend, check out https://git.pleroma.social/pleroma/pleroma-fe/wikis/dual-boot-with-qvitter to see how to run Pleroma-FE and Qvitter at the same time.
|
||||
|
||||
## Build Setup
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ module.exports = {
|
|||
],
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.runtime.common',
|
||||
'static': path.resolve(__dirname, '../static'),
|
||||
'src': path.resolve(__dirname, '../src'),
|
||||
'assets': path.resolve(__dirname, '../src/assets'),
|
||||
'components': path.resolve(__dirname, '../src/components')
|
||||
|
|
|
@ -19,32 +19,69 @@ There's currently no mechanism for user-settings synchronization across several
|
|||
|
||||
## Options
|
||||
|
||||
### `theme`
|
||||
Default theme used for new users. De-facto instance-default, user can change theme.
|
||||
### `alwaysShowSubjectInput`
|
||||
`true` - will always show subject line input, `false` - only show when it's not empty (i.e. replying). To hide subject line input completely, set it to `false` and `subjectLineBehavior` to `"noop"`
|
||||
|
||||
### `background`
|
||||
Default image background. Be aware of using too big images as they may take longer to load. Currently image is fitted with `background-size: cover` which means "scaled and cropped", currently left-aligned. De-facto instance default, user can choose their own background, if they remove their own background, instance default will be used instead.
|
||||
|
||||
### `collapseMessageWithSubject`
|
||||
Collapse post content when post has a subject line (content warning). Instance-default.
|
||||
|
||||
### `disableChat`
|
||||
hides the chat (TODO: even if it's enabled on backend)
|
||||
|
||||
### `greentext`
|
||||
Changes lines prefixed with the `>` character to have a green text color
|
||||
|
||||
### `hideFilteredStatuses`
|
||||
Removes filtered statuses from timelines.
|
||||
|
||||
### `hideMutedPosts`
|
||||
Removes muted statuses from timelines.
|
||||
|
||||
### `hidePostStats`
|
||||
Hide repeats/favorites counters for posts.
|
||||
|
||||
### `hideSitename`
|
||||
Hide instance name in header.
|
||||
|
||||
### `hideUserStats`
|
||||
Hide followers/friends counters for users.
|
||||
|
||||
### `loginMethod`
|
||||
`"password"` - show simple password field
|
||||
`"token"` - show button to log in with external method (will redirect to login form, more details in BE documentation)
|
||||
|
||||
### `logo`, `logoMask`, `logoMargin`
|
||||
Instance `logo`, could be any image, including svg. By default it assumes logo used will be monochrome-with-alpha one, this is done to be compatible with both light and dark themes, so that white logo designed with dark theme in mind won't be invisible over light theme, this is done via [CSS3 Masking](https://www.html5rocks.com/en/tutorials/masking/adobe/). Basically - it will take alpha channel of the image and fill non-transparent areas of it with solid color. If you really want colorful logo - it can be done by setting `logoMask` to `false`.
|
||||
|
||||
`logoMargin` allows you to adjust vertical margins between logo boundary and navbar borders. The idea is that to have logo's image without any extra margins and instead adjust them to your need in layout.
|
||||
|
||||
### `minimalScopesMode`
|
||||
Limit scope selection to *Direct*, *User default* and *Scope of post replying to*. This also makes it impossible to reply to a DM with a non-DM post from PleromaFE.
|
||||
|
||||
### `nsfwCensorImage`
|
||||
Use custom image for NSFW'd images
|
||||
|
||||
### `postContentType`
|
||||
Default post formatting option (markdown/bbcode/plaintext/etc...)
|
||||
|
||||
### `redirectRootNoLogin`, `redirectRootLogin`
|
||||
These two settings should point to where FE should redirect visitor when they login/open up website root
|
||||
|
||||
### `chatDisabled`
|
||||
hides the chat (TODO: even if it's enabled on backend)
|
||||
### `scopeCopy`
|
||||
Copy post scope (visibility) when replying to a post. Instance-default.
|
||||
|
||||
### `sidebarRight`
|
||||
Change alignment of sidebar and panels to the right. Defaults to `false`.
|
||||
|
||||
### `showFeaturesPanel`
|
||||
Show panel showcasing instance features/settings to logged-out visitors
|
||||
|
||||
### `showInstanceSpecificPanel`
|
||||
This allows you to include arbitrary HTML content in a panel below navigation menu. PleromaFE looks for an html page `instance/panel.html`, by default it's not provided in FE, but BE bundles some [default one](https://git.pleroma.social/pleroma/pleroma/blob/develop/priv/static/instance/panel.html). De-facto instance-defaults, since user can hide instance-specific panel.
|
||||
|
||||
### `collapseMessageWithSubject`
|
||||
Collapse post content when post has a subject line (content warning). Instance-default.
|
||||
|
||||
### `scopeCopy`
|
||||
Copy post scope (visibility) when replying to a post. Instance-default.
|
||||
|
||||
### `subjectLineBehavior`
|
||||
How to handle subject line (CW) when replying to a post.
|
||||
* `"email"` - like EMail - prepend `re: ` to subject line if it doesn't already start with it.
|
||||
|
@ -52,39 +89,22 @@ How to handle subject line (CW) when replying to a post.
|
|||
* `"noop"` - do not copy
|
||||
Instance-default.
|
||||
|
||||
### `postContentType`
|
||||
Default post formatting option (markdown/bbcode/plaintext/etc...)
|
||||
|
||||
### `alwaysShowSubjectInput`
|
||||
`true` - will always show subject line input, `false` - only show when it's not empty (i.e. replying). To hide subject line input completely, set it to `false` and `subjectLineBehavior` to `"noop"`
|
||||
|
||||
### `hidePostStats` and `hideUserStats`
|
||||
Hide counters for posts and users respectively, i.e. hiding repeats/favorites counts for posts, hiding followers/friends counts for users. This is just cosmetic and aimed to ease pressure and bias imposed by stat numbers of people and/or posts. (as an example: so that people care less about how many followers someone has since they can't see that info)
|
||||
|
||||
### `loginMethod`
|
||||
`"password"` - show simple password field
|
||||
`"token"` - show button to log in with external method (will redirect to login form, more details in BE documentation)
|
||||
### `theme`
|
||||
Default theme used for new users. De-facto instance-default, user can change theme.
|
||||
|
||||
### `webPushNotifications`
|
||||
Enables [PushAPI](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) - based notifications for users. Instance-default.
|
||||
|
||||
### `noAttachmentLinks`
|
||||
**TODO Currently doesn't seem to be doing anything code-wise**, but implication is to disable adding links for attachments, which looks nicer but breaks compatibility with old GNU/Social servers.
|
||||
|
||||
### `nsfwCensorImage`
|
||||
Use custom image for NSFW'd images
|
||||
|
||||
### `showFeaturesPanel`
|
||||
Show panel showcasing instance features/settings to logged-out visitors
|
||||
|
||||
### `hideSitename`
|
||||
Hide instance name in header
|
||||
|
||||
## Indirect configuration
|
||||
Some features are configured depending on how backend is configured. In general the approach is "if backend allows it there's no need to hide it, if backend doesn't allow it there's no need to show it.
|
||||
|
||||
### Chat
|
||||
**TODO somewhat broken, see: chatDisabled** chat can be disabled by disabling it in backend
|
||||
**TODO somewhat broken, see: disableChat** chat can be disabled by disabling it in backend
|
||||
|
||||
### Private Mode
|
||||
If the `private` instance setting is enabled in the backend, features that are not accessible without authentication, such as the timelines and search will be disabled for unauthenticated users.
|
||||
|
||||
### Rich text formatting in post formatting
|
||||
Rich text formatting options are displayed depending on how many formatting options are enabled on backend, if you don't want your users to use rich text at all you can only allow "text/plain" one, frontend then will only display post text format as a label instead of dropdown (just so that users know for example if you only allow Markdown, only BBCode or only Plain text)
|
||||
|
@ -92,13 +112,3 @@ Rich text formatting options are displayed depending on how many formatting opti
|
|||
### Who to follow
|
||||
This is a panel intended for users to find people to follow based on randomness or on post contents. Being potentially privacy unfriendly feature it needs to be enabled and configured in backend to be enabled.
|
||||
|
||||
### Safe DM message display
|
||||
|
||||
Setting this will change the warning text that is displayed for direct messages.
|
||||
|
||||
ATTENTION: If you actually want the behavior to change. You will need to set the appropriate option at the backend. See the backend documentation for information about that.
|
||||
|
||||
DO NOT activate this without checking the backend configuration first!
|
||||
|
||||
### Private Mode
|
||||
If the `private` instance setting is enabled in the backend, features that are not accessible without authentication, such as the timelines and search will be disabled for unauthenticated users.
|
||||
|
|
|
@ -33,7 +33,7 @@ will become
|
|||
Note that you can only use emoji defined on your instance, you cannot "copy" someone else's emoji, and will have to ask your administrator to copy emoji from other instance to yours.
|
||||
Lastly, there's two convenience options for emoji: an emoji picker (smiley face to the right of "submit" button) and autocomplete suggestions - when you start typing :shortcode: it will automatically try to suggest you emoj and complete the shortcode for you if you select one. **Note** that if emoji doesn't show up in suggestions nor in emoji picker it means there's no such emoji on your instance, if shortcode doesn't match any defined emoji it will appear as text.
|
||||
* **Attachments** are fairly simple - you can attach any file to a post as long as the file is within maximum size limits. If you're uploading explicit material you can mark all of your attachments as sensitive (or add `#nsfw` tag) - it will hide the images and videos behind a warning so that it won't be displayed instantly.
|
||||
* **Subject line** also known as **CW** (Content Warning) could be used as a header to the post and/or to warn others about contents of the post having something that might upset somebody or something among those lines. Several applications allow to hide post content leaving only subject line visible. As a side-effect using subject line will also mark your images as sensitive (see above).
|
||||
* **Subject line** also known as **CW** (Content Warning) could be used as a header to the post and/or to warn others about contents of the post having something that might upset somebody or something among those lines. Several applications allow to hide post content leaving only subject line visible. Using a subject line will not mark your images as sensitive, you will have to do that explicitly (see above).
|
||||
* **Visiblity scope** controls who will be able to see your posts. There are four scopes available:
|
||||
|
||||
1. `Public`: This is the default, and some fediverse software like GNU Social only supports this. This means that your post is accessible by anyone and will be shown in the public timelines.
|
||||
|
|
12
package.json
12
package.json
|
@ -22,24 +22,20 @@
|
|||
"cropperjs": "^1.4.3",
|
||||
"diff": "^3.0.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"karma-mocha-reporter": "^2.2.1",
|
||||
"localforage": "^1.5.0",
|
||||
"object-path": "^0.11.3",
|
||||
"phoenix": "^1.3.0",
|
||||
"portal-vue": "^2.1.4",
|
||||
"sanitize-html": "^1.13.0",
|
||||
"v-click-outside": "^2.1.1",
|
||||
"v-tooltip": "^2.0.2",
|
||||
"vue": "^2.5.13",
|
||||
"vue": "^2.6.11",
|
||||
"vue-chat-scroll": "^1.2.1",
|
||||
"vue-i18n": "^7.3.2",
|
||||
"vue-router": "^3.0.1",
|
||||
"vue-template-compiler": "^2.3.4",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuelidate": "^0.7.4",
|
||||
"vuex": "^3.0.1",
|
||||
"whatwg-fetch": "^2.0.3"
|
||||
"vuex": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"karma-mocha-reporter": "^2.2.1",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.6",
|
||||
|
|
15
src/App.js
15
src/App.js
|
@ -6,6 +6,7 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
|
|||
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'
|
||||
import SettingsModal from './components/settings_modal/settings_modal.vue'
|
||||
import MediaModal from './components/media_modal/media_modal.vue'
|
||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||
|
@ -29,6 +30,7 @@ export default {
|
|||
SideDrawer,
|
||||
MobilePostStatusButton,
|
||||
MobileNav,
|
||||
SettingsModal,
|
||||
UserReportingModal,
|
||||
PostStatusModal
|
||||
},
|
||||
|
@ -45,7 +47,8 @@ export default {
|
|||
}),
|
||||
created () {
|
||||
// Load the locale from the storage
|
||||
this.$i18n.locale = this.$store.getters.mergedConfig.interfaceLanguage
|
||||
const val = this.$store.getters.mergedConfig.interfaceLanguage
|
||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
window.addEventListener('resize', this.updateMobileState)
|
||||
},
|
||||
destroyed () {
|
||||
|
@ -99,7 +102,12 @@ export default {
|
|||
},
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
isMobileLayout () { return this.$store.state.interface.mobileLayout },
|
||||
privateMode () { return this.$store.state.instance.private }
|
||||
privateMode () { return this.$store.state.instance.private },
|
||||
sidebarAlign () {
|
||||
return {
|
||||
'order': this.$store.state.instance.sidebarRight ? 99 : 0
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scrollToTop () {
|
||||
|
@ -112,6 +120,9 @@ export default {
|
|||
onSearchBarToggled (hidden) {
|
||||
this.searchBarHidden = hidden
|
||||
},
|
||||
openSettingsModal () {
|
||||
this.$store.dispatch('openSettingsModal')
|
||||
},
|
||||
updateMobileState () {
|
||||
const mobileLayout = windowWidth() <= 800
|
||||
const changed = mobileLayout !== this.isMobileLayout
|
||||
|
|
163
src/App.scss
163
src/App.scss
|
@ -31,9 +31,12 @@ h4 {
|
|||
margin: auto;
|
||||
min-height: 100vh;
|
||||
max-width: 980px;
|
||||
background-color: rgba(0,0,0,0.15);
|
||||
align-content: flex-start;
|
||||
}
|
||||
.underlay {
|
||||
background-color: rgba(0,0,0,0.15);
|
||||
background-color: var(--underlay, rgba(0,0,0,0.15));
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
|
@ -98,18 +101,39 @@ button {
|
|||
&:active {
|
||||
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: var(--buttonPressedShadow);
|
||||
color: $fallback--text;
|
||||
color: var(--btnPressedText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnPressed, $fallback--fg);
|
||||
i {
|
||||
color: $fallback--text;
|
||||
color: var(--btnPressedText, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
color: $fallback--text;
|
||||
color: var(--btnDisabledText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnDisabled, $fallback--fg);
|
||||
i {
|
||||
color: $fallback--text;
|
||||
color: var(--btnDisabledText, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
&.pressed {
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg)
|
||||
&.toggled {
|
||||
color: $fallback--text;
|
||||
color: var(--btnToggledText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnToggled, $fallback--fg);
|
||||
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: var(--buttonPressedShadow);
|
||||
i {
|
||||
color: $fallback--text;
|
||||
color: var(--btnToggledText, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
|
@ -121,12 +145,15 @@ button {
|
|||
}
|
||||
}
|
||||
|
||||
label.select {
|
||||
padding: 0;
|
||||
input, textarea, .select, .input {
|
||||
|
||||
}
|
||||
&.unstyled {
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
height: unset;
|
||||
}
|
||||
|
||||
input, textarea, .select {
|
||||
border: none;
|
||||
border-radius: $fallback--inputRadius;
|
||||
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||
|
@ -140,13 +167,17 @@ input, textarea, .select {
|
|||
font-family: var(--inputFont, sans-serif);
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 8px .5em;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
height: 28px;
|
||||
line-height: 16px;
|
||||
hyphens: none;
|
||||
padding: 8px .5em;
|
||||
|
||||
&.select {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:disabled, &[disabled=disabled] {
|
||||
cursor: not-allowed;
|
||||
|
@ -160,7 +191,7 @@ input, textarea, .select {
|
|||
right: 5px;
|
||||
height: 100%;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
color: var(--inputText, $fallback--text);
|
||||
line-height: 28px;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
|
@ -198,7 +229,7 @@ input, textarea, .select {
|
|||
&:checked + label::before {
|
||||
box-shadow: 0px 0px 2px black inset, 0px 0px 0px 4px $fallback--fg inset;
|
||||
box-shadow: var(--inputShadow), 0px 0px 0px 4px var(--fg, $fallback--fg) inset;
|
||||
background-color: var(--link, $fallback--link);
|
||||
background-color: var(--accent, $fallback--link);
|
||||
}
|
||||
&:disabled {
|
||||
&,
|
||||
|
@ -235,7 +266,7 @@ input, textarea, .select {
|
|||
display: none;
|
||||
&:checked + label::before {
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
color: var(--inputText, $fallback--text);
|
||||
}
|
||||
&:disabled {
|
||||
&,
|
||||
|
@ -353,6 +384,33 @@ i[class*=icon-] {
|
|||
height: 50px;
|
||||
box-sizing: border-box;
|
||||
|
||||
button {
|
||||
&, i[class*=icon-] {
|
||||
color: $fallback--text;
|
||||
color: var(--btnTopBarText, $fallback--text);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnPressedTopBar, $fallback--fg);
|
||||
color: $fallback--text;
|
||||
color: var(--btnPressedTopBarText, $fallback--text);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: $fallback--text;
|
||||
color: var(--btnDisabledTopBarText, $fallback--text);
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
color: $fallback--text;
|
||||
color: var(--btnToggledTopBarText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnToggledTopBar, $fallback--fg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
|
@ -487,6 +545,10 @@ main-router {
|
|||
color: $fallback--faint;
|
||||
color: var(--panelFaint, $fallback--faint);
|
||||
}
|
||||
.faint-link {
|
||||
color: $fallback--faint;
|
||||
color: var(--faintLink, $fallback--faint);
|
||||
}
|
||||
|
||||
.alert {
|
||||
white-space: nowrap;
|
||||
|
@ -504,11 +566,35 @@ main-router {
|
|||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
margin-left: .25em;
|
||||
margin-left: .5em;
|
||||
min-width: 1px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
button {
|
||||
&, i[class*=icon-] {
|
||||
color: $fallback--text;
|
||||
color: var(--btnPanelText, $fallback--text);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnPressedPanel, $fallback--fg);
|
||||
color: $fallback--text;
|
||||
color: var(--btnPressedPanelText, $fallback--text);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: $fallback--text;
|
||||
color: var(--btnDisabledPanelText, $fallback--text);
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
color: $fallback--text;
|
||||
color: var(--btnToggledPanelText, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $fallback--link;
|
||||
color: var(--panelLink, $fallback--link)
|
||||
|
@ -774,51 +860,6 @@ nav {
|
|||
}
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
border-bottom: 2px solid var(--fg, $fallback--fg);
|
||||
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%;
|
||||
max-width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.unavailable,
|
||||
.unavailable i {
|
||||
color: var(--cRed, $fallback--cRed);
|
||||
color: $fallback--cRed;
|
||||
}
|
||||
|
||||
.btn {
|
||||
min-height: 28px;
|
||||
min-width: 10em;
|
||||
padding: 0 2em;
|
||||
}
|
||||
|
||||
.number-input {
|
||||
max-width: 6em;
|
||||
}
|
||||
}
|
||||
.select-multiple {
|
||||
display: flex;
|
||||
.option-list {
|
||||
|
|
15
src/App.vue
15
src/App.vue
|
@ -46,15 +46,16 @@
|
|||
@toggled="onSearchBarToggled"
|
||||
@click.stop.native
|
||||
/>
|
||||
<router-link
|
||||
<a
|
||||
href="#"
|
||||
class="mobile-hidden"
|
||||
:to="{ name: 'settings'}"
|
||||
@click.stop="openSettingsModal"
|
||||
>
|
||||
<i
|
||||
class="button-icon icon-cog nav-icon"
|
||||
:title="$t('nav.preferences')"
|
||||
/>
|
||||
</router-link>
|
||||
</a>
|
||||
<a
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
href="/pleroma/admin/#/login-pleroma"
|
||||
|
@ -78,9 +79,12 @@
|
|||
</nav>
|
||||
<div
|
||||
id="content"
|
||||
class="container"
|
||||
class="container underlay"
|
||||
>
|
||||
<div
|
||||
class="sidebar-flexer mobile-hidden"
|
||||
:style="sidebarAlign"
|
||||
>
|
||||
<div class="sidebar-flexer mobile-hidden">
|
||||
<div class="sidebar-bounds">
|
||||
<div class="sidebar-scroller">
|
||||
<div class="sidebar">
|
||||
|
@ -122,6 +126,7 @@
|
|||
<MobilePostStatusButton />
|
||||
<UserReportingModal />
|
||||
<PostStatusModal />
|
||||
<SettingsModal />
|
||||
<portal-target name="modal" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -5,6 +5,8 @@ import App from '../App.vue'
|
|||
import { windowWidth } from '../services/window_utils/window_utils'
|
||||
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
||||
import { applyTheme } from '../services/style_setter/style_setter.js'
|
||||
|
||||
const getStatusnetConfig = async ({ store }) => {
|
||||
try {
|
||||
|
@ -106,9 +108,9 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
copyInstanceOption('subjectLineBehavior')
|
||||
copyInstanceOption('postContentType')
|
||||
copyInstanceOption('alwaysShowSubjectInput')
|
||||
copyInstanceOption('noAttachmentLinks')
|
||||
copyInstanceOption('showFeaturesPanel')
|
||||
copyInstanceOption('hideSitename')
|
||||
copyInstanceOption('sidebarRight')
|
||||
|
||||
return store.dispatch('setTheme', config['theme'])
|
||||
}
|
||||
|
@ -221,9 +223,16 @@ const getNodeInfo = async ({ store }) => {
|
|||
|
||||
const frontendVersion = window.___pleromafe_commit_hash
|
||||
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
|
||||
store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') })
|
||||
|
||||
const federation = metadata.federation
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'tagPolicyAvailable',
|
||||
value: typeof federation.mrf_policies === 'undefined'
|
||||
? false
|
||||
: metadata.federation.mrf_policies.includes('TagPolicy')
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federating',
|
||||
|
@ -232,6 +241,9 @@ const getNodeInfo = async ({ store }) => {
|
|||
: federation.enabled
|
||||
})
|
||||
|
||||
const accountActivationRequired = metadata.accountActivationRequired
|
||||
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
|
||||
|
||||
const accounts = metadata.staffAccounts
|
||||
resolveStaffAccounts({ store, accounts })
|
||||
} else {
|
||||
|
@ -258,7 +270,7 @@ const checkOAuthToken = async ({ store }) => {
|
|||
try {
|
||||
await store.dispatch('loginUser', store.getters.getUserToken())
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
resolve()
|
||||
|
@ -266,29 +278,38 @@ const checkOAuthToken = async ({ store }) => {
|
|||
}
|
||||
|
||||
const afterStoreSetup = async ({ store, i18n }) => {
|
||||
if (store.state.config.customTheme) {
|
||||
// This is a hack to deal with async loading of config.json and themes
|
||||
// See: style_setter.js, setPreset()
|
||||
window.themeLoaded = true
|
||||
store.dispatch('setOption', {
|
||||
name: 'customTheme',
|
||||
value: store.state.config.customTheme
|
||||
})
|
||||
}
|
||||
|
||||
const width = windowWidth()
|
||||
store.dispatch('setMobileLayout', width <= 800)
|
||||
await setConfig({ store })
|
||||
|
||||
const { customTheme, customThemeSource } = store.state.config
|
||||
const { theme } = store.state.instance
|
||||
const customThemePresent = customThemeSource || customTheme
|
||||
|
||||
if (customThemePresent) {
|
||||
if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) {
|
||||
applyTheme(customThemeSource)
|
||||
} else {
|
||||
applyTheme(customTheme)
|
||||
}
|
||||
} else if (theme) {
|
||||
// do nothing, it will load asynchronously
|
||||
} else {
|
||||
console.error('Failed to load any theme!')
|
||||
}
|
||||
|
||||
// Now we can try getting the server settings and logging in
|
||||
await Promise.all([
|
||||
checkOAuthToken({ store }),
|
||||
setConfig({ store }),
|
||||
getTOS({ store }),
|
||||
getInstancePanel({ store }),
|
||||
getStickers({ store }),
|
||||
getNodeInfo({ store })
|
||||
])
|
||||
|
||||
// Start fetching things that don't need to block the UI
|
||||
store.dispatch('fetchMutes')
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
routes: routes(store),
|
||||
|
|
|
@ -7,10 +7,8 @@ import Interactions from 'components/interactions/interactions.vue'
|
|||
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
||||
import UserProfile from 'components/user_profile/user_profile.vue'
|
||||
import Search from 'components/search/search.vue'
|
||||
import Settings from 'components/settings/settings.vue'
|
||||
import Registration from 'components/registration/registration.vue'
|
||||
import PasswordReset from 'components/password_reset/password_reset.vue'
|
||||
import UserSettings from 'components/user_settings/user_settings.vue'
|
||||
import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
||||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||
import Notifications from 'components/notifications/notifications.vue'
|
||||
|
@ -56,12 +54,10 @@ export default (store) => {
|
|||
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
|
||||
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'settings', path: '/settings', component: Settings },
|
||||
{ name: 'registration', path: '/registration', component: Registration },
|
||||
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
|
||||
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'login', path: '/login', component: AuthForm },
|
||||
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
|
||||
const AccountActions = {
|
||||
props: [
|
||||
'user'
|
||||
'user', 'relationship'
|
||||
],
|
||||
data () {
|
||||
return { }
|
||||
},
|
||||
components: {
|
||||
ProgressButton
|
||||
ProgressButton,
|
||||
Popover
|
||||
},
|
||||
methods: {
|
||||
showRepeats () {
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
<template>
|
||||
<div class="account-actions">
|
||||
<v-popover
|
||||
<Popover
|
||||
trigger="click"
|
||||
class="account-tools-popover"
|
||||
:container="false"
|
||||
placement="bottom-end"
|
||||
:offset="5"
|
||||
placement="bottom"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
class="account-tools-popover"
|
||||
>
|
||||
<div slot="popover">
|
||||
<div class="dropdown-menu">
|
||||
<template v-if="user.following">
|
||||
<template v-if="relationship.following">
|
||||
<button
|
||||
v-if="user.showing_reblogs"
|
||||
v-if="relationship.showing_reblogs"
|
||||
class="btn btn-default dropdown-item"
|
||||
@click="hideRepeats"
|
||||
>
|
||||
{{ $t('user_card.hide_repeats') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="!user.showing_reblogs"
|
||||
v-if="!relationship.showing_reblogs"
|
||||
class="btn btn-default dropdown-item"
|
||||
@click="showRepeats"
|
||||
>
|
||||
|
@ -30,7 +30,7 @@
|
|||
/>
|
||||
</template>
|
||||
<button
|
||||
v-if="user.statusnet_blocking"
|
||||
v-if="relationship.blocking"
|
||||
class="btn btn-default btn-block dropdown-item"
|
||||
@click="unblockUser"
|
||||
>
|
||||
|
@ -51,10 +51,13 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn btn-default ellipsis-button">
|
||||
<div
|
||||
slot="trigger"
|
||||
class="btn btn-default ellipsis-button"
|
||||
>
|
||||
<i class="icon-ellipsis trigger-button" />
|
||||
</div>
|
||||
</v-popover>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -62,7 +65,6 @@
|
|||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../popper/popper.scss';
|
||||
.account-actions {
|
||||
margin: 0 .8em;
|
||||
}
|
||||
|
@ -70,6 +72,7 @@
|
|||
.account-actions button.dropdown-item {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.account-actions .trigger-button {
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<div class="async-component-error">
|
||||
<div>
|
||||
<h4>
|
||||
{{ $t('general.generic_error') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ $t('general.error_retry') }}
|
||||
</p>
|
||||
<button
|
||||
class="btn"
|
||||
@click="retry"
|
||||
>
|
||||
{{ $t('general.retry') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
retry () {
|
||||
this.$emit('resetAsyncComponent')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.async-component-error {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.btn {
|
||||
margin: .5em;
|
||||
padding: .5em 2em;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -130,6 +130,8 @@
|
|||
.placeholder {
|
||||
margin-right: 8px;
|
||||
margin-bottom: 4px;
|
||||
color: $fallback--link;
|
||||
color: var(--postLink, $fallback--link);
|
||||
}
|
||||
|
||||
.nsfw-placeholder {
|
||||
|
|
|
@ -40,8 +40,8 @@
|
|||
top: 100%;
|
||||
right: 0;
|
||||
max-height: 400px;
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: $fallback--border;
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
class="basic-user-card-expanded-content"
|
||||
>
|
||||
<UserCard
|
||||
:user="user"
|
||||
:user-id="user.id"
|
||||
:rounded="true"
|
||||
:bordered="true"
|
||||
/>
|
||||
|
|
|
@ -11,8 +11,11 @@ const BlockCard = {
|
|||
user () {
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
relationship () {
|
||||
return this.$store.getters.relationship(this.userId)
|
||||
},
|
||||
blocked () {
|
||||
return this.user.statusnet_blocking
|
||||
return this.relationship.blocking
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -87,13 +87,13 @@ export default {
|
|||
|
||||
&:checked + .checkbox-indicator::before {
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
color: var(--inputText, $fallback--text);
|
||||
}
|
||||
|
||||
&:indeterminate + .checkbox-indicator::before {
|
||||
content: '–';
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
color: var(--inputText, $fallback--text);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
68
src/components/color_input/color_input.scss
Normal file
68
src/components/color_input/color_input.scss
Normal file
|
@ -0,0 +1,68 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.color-input {
|
||||
display: inline-flex;
|
||||
|
||||
&-field.input {
|
||||
display: inline-flex;
|
||||
flex: 0 0 0;
|
||||
max-width: 9em;
|
||||
align-items: stretch;
|
||||
padding: .2em 8px;
|
||||
|
||||
input {
|
||||
background: none;
|
||||
color: $fallback--lightText;
|
||||
color: var(--inputText, $fallback--lightText);
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
&.textColor {
|
||||
flex: 1 0 3em;
|
||||
min-width: 3em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.nativeColor {
|
||||
flex: 0 0 2em;
|
||||
min-width: 2em;
|
||||
align-self: center;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.computedIndicator,
|
||||
.transparentIndicator {
|
||||
flex: 0 0 2em;
|
||||
min-width: 2em;
|
||||
align-self: center;
|
||||
height: 100%;
|
||||
}
|
||||
.transparentIndicator {
|
||||
// forgot to install counter-strike source, ooops
|
||||
background-color: #FF00FF;
|
||||
position: relative;
|
||||
&::before, &::after {
|
||||
display: block;
|
||||
content: '';
|
||||
background-color: #000000;
|
||||
position: absolute;
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
&::after {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
&::before {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
class="color-control style-control"
|
||||
class="color-input style-control"
|
||||
:class="{ disabled: !present || disabled }"
|
||||
>
|
||||
<label
|
||||
|
@ -9,46 +9,100 @@
|
|||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<input
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
:id="name + '-o'"
|
||||
class="opt exlcude-disabled"
|
||||
type="checkbox"
|
||||
<Checkbox
|
||||
v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
|
||||
:checked="present"
|
||||
@input="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
|
||||
>
|
||||
<label
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
class="opt-l"
|
||||
:for="name + '-o'"
|
||||
:disabled="disabled"
|
||||
class="opt"
|
||||
@change="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
|
||||
/>
|
||||
<input
|
||||
:id="name"
|
||||
class="color-input"
|
||||
type="color"
|
||||
:value="value || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<div class="input color-input-field">
|
||||
<input
|
||||
:id="name + '-t'"
|
||||
class="text-input"
|
||||
class="textColor unstyled"
|
||||
type="text"
|
||||
:value="value || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<input
|
||||
v-if="validColor"
|
||||
:id="name"
|
||||
class="nativeColor unstyled"
|
||||
type="color"
|
||||
:value="value || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<div
|
||||
v-if="transparentColor"
|
||||
class="transparentIndicator"
|
||||
/>
|
||||
<div
|
||||
v-if="computedColor"
|
||||
class="computedIndicator"
|
||||
:style="{backgroundColor: fallback}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" src="./color_input.scss"></style>
|
||||
<script>
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
export default {
|
||||
props: [
|
||||
'name', 'label', 'value', 'fallback', 'disabled'
|
||||
],
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
props: {
|
||||
// Name of color, used for identifying
|
||||
name: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
// Readable label
|
||||
label: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
// Color value, should be required but vue cannot tell the difference
|
||||
// between "property missing" and "property set to undefined"
|
||||
value: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
// Color fallback to use when value is not defeind
|
||||
fallback: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
// Disable the control
|
||||
disabled: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Show "optional" tickbox, for when value might become mandatory
|
||||
showOptionalTickbox: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
present () {
|
||||
return typeof this.value !== 'undefined'
|
||||
},
|
||||
validColor () {
|
||||
return hex2rgb(this.value || this.fallback)
|
||||
},
|
||||
transparentColor () {
|
||||
return this.value === 'transparent'
|
||||
},
|
||||
computedColor () {
|
||||
return this.value && this.value.startsWith('--')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,9 +37,17 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
'large', 'contrast'
|
||||
],
|
||||
props: {
|
||||
large: {
|
||||
required: false
|
||||
},
|
||||
// TODO: Make theme switcher compute theme initially so that contrast
|
||||
// component won't be called without contrast data
|
||||
contrast: {
|
||||
required: false,
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hint () {
|
||||
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
|
||||
|
|
|
@ -75,18 +75,18 @@
|
|||
.dialog-modal-content {
|
||||
margin: 0;
|
||||
padding: 1rem 1rem;
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.dialog-modal-footer {
|
||||
margin: 0;
|
||||
padding: .5em .5em;
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
border-top: 1px solid $fallback--bg;
|
||||
border-top: 1px solid var(--bg, $fallback--bg);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
border-top: 1px solid $fallback--border;
|
||||
border-top: 1px solid var(--border, $fallback--border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
|
|
|
@ -5,9 +5,20 @@ const DomainMuteCard = {
|
|||
components: {
|
||||
ProgressButton
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
muted () {
|
||||
return this.user.domainMutes.includes(this.domain)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
unmuteDomain () {
|
||||
return this.$store.dispatch('unmuteDomain', this.domain)
|
||||
},
|
||||
muteDomain () {
|
||||
return this.$store.dispatch('muteDomain', this.domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
{{ domain }}
|
||||
</div>
|
||||
<ProgressButton
|
||||
v-if="muted"
|
||||
:click="unmuteDomain"
|
||||
class="btn btn-default"
|
||||
>
|
||||
|
@ -12,6 +13,16 @@
|
|||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-else
|
||||
:click="muteDomain"
|
||||
class="btn btn-default"
|
||||
>
|
||||
{{ $t('domain_mute_card.mute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('domain_mute_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -34,5 +45,9 @@
|
|||
button {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
.autosuggest-results & {
|
||||
padding-left: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -147,7 +147,7 @@ const EmojiInput = {
|
|||
input.elm.addEventListener('keydown', this.onKeyDown)
|
||||
input.elm.addEventListener('click', this.onClickInput)
|
||||
input.elm.addEventListener('transitionend', this.onTransition)
|
||||
input.elm.addEventListener('compositionupdate', this.onCompositionUpdate)
|
||||
input.elm.addEventListener('input', this.onInput)
|
||||
},
|
||||
unmounted () {
|
||||
const { input } = this
|
||||
|
@ -159,7 +159,7 @@ const EmojiInput = {
|
|||
input.elm.removeEventListener('keydown', this.onKeyDown)
|
||||
input.elm.removeEventListener('click', this.onClickInput)
|
||||
input.elm.removeEventListener('transitionend', this.onTransition)
|
||||
input.elm.removeEventListener('compositionupdate', this.onCompositionUpdate)
|
||||
input.elm.removeEventListener('input', this.onInput)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -406,12 +406,6 @@ const EmojiInput = {
|
|||
this.resize()
|
||||
this.$emit('input', e.target.value)
|
||||
},
|
||||
onCompositionUpdate (e) {
|
||||
this.showPicker = false
|
||||
this.setCaret(e)
|
||||
this.resize()
|
||||
this.$emit('input', e.target.value)
|
||||
},
|
||||
onClickInput (e) {
|
||||
this.showPicker = false
|
||||
},
|
||||
|
|
|
@ -109,10 +109,16 @@
|
|||
box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--popupShadow);
|
||||
min-width: 75%;
|
||||
background: $fallback--bg;
|
||||
background: var(--bg, $fallback--bg);
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--popover, $fallback--bg);
|
||||
color: $fallback--link;
|
||||
color: var(--popoverText, $fallback--link);
|
||||
--faint: var(--popoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||
--postLink: var(--popoverPostLink, $fallback--link);
|
||||
--postFaintLink: var(--popoverPostFaintLink, $fallback--link);
|
||||
--icon: var(--popoverIcon, $fallback--icon);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,7 +163,12 @@
|
|||
|
||||
&.highlighted {
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--lightBg, $fallback--fg);
|
||||
background-color: var(--selectedMenuPopover, $fallback--fg);
|
||||
color: var(--selectedMenuPopoverText, $fallback--text);
|
||||
--faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,17 +29,29 @@ export default data => input => {
|
|||
export const suggestEmoji = emojis => input => {
|
||||
const noPrefix = input.toLowerCase().substr(1)
|
||||
return emojis
|
||||
.filter(({ displayText }) => displayText.toLowerCase().startsWith(noPrefix))
|
||||
.filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
|
||||
.sort((a, b) => {
|
||||
let aScore = 0
|
||||
let bScore = 0
|
||||
|
||||
// Make custom emojis a priority
|
||||
aScore += a.imageUrl ? 10 : 0
|
||||
bScore += b.imageUrl ? 10 : 0
|
||||
// An exact match always wins
|
||||
aScore += a.displayText.toLowerCase() === noPrefix ? 200 : 0
|
||||
bScore += b.displayText.toLowerCase() === noPrefix ? 200 : 0
|
||||
|
||||
// Sort alphabetically
|
||||
const alphabetically = a.displayText > b.displayText ? 1 : -1
|
||||
// Prioritize custom emoji a lot
|
||||
aScore += a.imageUrl ? 100 : 0
|
||||
bScore += b.imageUrl ? 100 : 0
|
||||
|
||||
// Prioritize prefix matches somewhat
|
||||
aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
|
||||
bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
|
||||
|
||||
// Sort by length
|
||||
aScore -= a.displayText.length
|
||||
bScore -= b.displayText.length
|
||||
|
||||
// Break ties alphabetically
|
||||
const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5
|
||||
|
||||
return bScore - aScore + alphabetically
|
||||
})
|
||||
|
|
|
@ -8,6 +8,15 @@
|
|||
left: 0;
|
||||
margin: 0 !important;
|
||||
z-index: 1;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--popover, $fallback--bg);
|
||||
color: $fallback--link;
|
||||
color: var(--popoverText, $fallback--link);
|
||||
--lightText: var(--popoverLightText, $fallback--faint);
|
||||
--faint: var(--popoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||
--icon: var(--popoverIcon, $fallback--icon);
|
||||
|
||||
.keep-open,
|
||||
.too-many-emoji {
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
|
||||
const EMOJI_REACTION_COUNT_CUTOFF = 12
|
||||
|
||||
const EmojiReactions = {
|
||||
name: 'EmojiReactions',
|
||||
components: {
|
||||
UserAvatar
|
||||
UserAvatar,
|
||||
Popover
|
||||
},
|
||||
props: ['status'],
|
||||
data: () => ({
|
||||
showAll: false,
|
||||
popperOptions: {
|
||||
modifiers: {
|
||||
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
|
||||
}
|
||||
}
|
||||
showAll: false
|
||||
}),
|
||||
computed: {
|
||||
tooManyReactions () {
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
<template>
|
||||
<div class="emoji-reactions">
|
||||
<v-popover
|
||||
<Popover
|
||||
v-for="(reaction) in emojiReactions"
|
||||
:key="reaction.name"
|
||||
:popper-options="popperOptions"
|
||||
trigger="hover"
|
||||
placement="top"
|
||||
:offset="{ y: 5 }"
|
||||
>
|
||||
|
||||
<div
|
||||
slot="popover"
|
||||
slot="content"
|
||||
class="reacted-users"
|
||||
>
|
||||
<div v-if="accountsForEmoji[reaction.name].length">
|
||||
|
@ -24,7 +23,12 @@
|
|||
:compact="true"
|
||||
/>
|
||||
<div class="reacted-user-names">
|
||||
<span class="reacted-user-name" v-html="account.name_html" />
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
class="reacted-user-name"
|
||||
v-html="account.name_html"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<span class="reacted-user-screen-name">{{ account.screen_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -34,6 +38,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="emoji-reaction btn btn-default"
|
||||
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
|
||||
@click="emojiOnClick(reaction.name, $event)"
|
||||
|
@ -42,17 +47,16 @@
|
|||
<span class="reaction-emoji">{{ reaction.name }}</span>
|
||||
<span>{{ reaction.count }}</span>
|
||||
</button>
|
||||
</v-popover>
|
||||
</Popover>
|
||||
<a
|
||||
v-if="tooManyReactions"
|
||||
@click="toggleShowAll"
|
||||
class="emoji-reaction-expand faint"
|
||||
href='javascript:void(0)'
|
||||
href="javascript:void(0)"
|
||||
@click="toggleShowAll"
|
||||
>
|
||||
{{ showAll ? $t('general.show_less') : showMoreString }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script src="./emoji_reactions.js" ></script>
|
||||
|
@ -78,6 +82,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 0.5em;
|
||||
min-width: 5em;
|
||||
|
||||
img {
|
||||
width: 1em;
|
||||
|
@ -128,7 +133,7 @@
|
|||
}
|
||||
|
||||
.picked-reaction {
|
||||
border: 1px solid var(--link, $fallback--link);
|
||||
border: 1px solid var(--accent, $fallback--link);
|
||||
margin-left: -1px; // offset the border, can't use inset shadows either
|
||||
margin-right: calc(0.5em - 1px);
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
exportData () {
|
||||
const stringified = JSON.stringify(this.exportObject) // Pretty-print and indent with 2 spaces
|
||||
const stringified = JSON.stringify(this.exportObject, null, 2) // Pretty-print and indent with 2 spaces
|
||||
|
||||
// Create an invisible link with a data url and simulate a click
|
||||
const e = document.createElement('a')
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import Popover from '../popover/popover.vue'
|
||||
|
||||
const ExtraButtons = {
|
||||
props: [ 'status' ],
|
||||
components: { Popover },
|
||||
methods: {
|
||||
deleteStatus () {
|
||||
const confirmed = window.confirm(this.$t('status.delete_confirm'))
|
||||
|
@ -26,6 +29,11 @@ const ExtraButtons = {
|
|||
this.$store.dispatch('unmuteConversation', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
copyLink () {
|
||||
navigator.clipboard.writeText(this.statusLink)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -43,6 +51,9 @@ const ExtraButtons = {
|
|||
},
|
||||
canMute () {
|
||||
return !!this.currentUser
|
||||
},
|
||||
statusLink () {
|
||||
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<template>
|
||||
<v-popover
|
||||
v-if="canDelete || canMute || canPin"
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="top"
|
||||
class="extra-button-popover"
|
||||
>
|
||||
<div slot="popover">
|
||||
<div
|
||||
slot="content"
|
||||
slot-scope="{close}"
|
||||
>
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
v-if="canMute && !status.thread_muted"
|
||||
|
@ -23,41 +25,48 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="!status.pinned && canPin"
|
||||
v-close-popover
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="pinStatus"
|
||||
@click="close"
|
||||
>
|
||||
<i class="icon-pin" /><span>{{ $t("status.pin") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="status.pinned && canPin"
|
||||
v-close-popover
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="unpinStatus"
|
||||
@click="close"
|
||||
>
|
||||
<i class="icon-pin" /><span>{{ $t("status.unpin") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDelete"
|
||||
v-close-popover
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="deleteStatus"
|
||||
@click="close"
|
||||
>
|
||||
<i class="icon-cancel" /><span>{{ $t("status.delete") }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="copyLink"
|
||||
@click="close"
|
||||
>
|
||||
<i class="icon-share" /><span>{{ $t("status.copy_link") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-icon">
|
||||
<i class="icon-ellipsis" />
|
||||
</div>
|
||||
</v-popover>
|
||||
<i
|
||||
slot="trigger"
|
||||
class="icon-ellipsis button-icon"
|
||||
/>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script src="./extra_buttons.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../popper/popper.scss';
|
||||
|
||||
.icon-ellipsis {
|
||||
cursor: pointer;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||
export default {
|
||||
props: ['user', 'labelFollowing', 'buttonClass'],
|
||||
props: ['relationship', 'labelFollowing', 'buttonClass'],
|
||||
data () {
|
||||
return {
|
||||
inProgress: false
|
||||
|
@ -8,12 +8,12 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
isPressed () {
|
||||
return this.inProgress || this.user.following
|
||||
return this.inProgress || this.relationship.following
|
||||
},
|
||||
title () {
|
||||
if (this.inProgress || this.user.following) {
|
||||
if (this.inProgress || this.relationship.following) {
|
||||
return this.$t('user_card.follow_unfollow')
|
||||
} else if (this.user.requested) {
|
||||
} else if (this.relationship.requested) {
|
||||
return this.$t('user_card.follow_again')
|
||||
} else {
|
||||
return this.$t('user_card.follow')
|
||||
|
@ -22,9 +22,9 @@ export default {
|
|||
label () {
|
||||
if (this.inProgress) {
|
||||
return this.$t('user_card.follow_progress')
|
||||
} else if (this.user.following) {
|
||||
} else if (this.relationship.following) {
|
||||
return this.labelFollowing || this.$t('user_card.following')
|
||||
} else if (this.user.requested) {
|
||||
} else if (this.relationship.requested) {
|
||||
return this.$t('user_card.follow_sent')
|
||||
} else {
|
||||
return this.$t('user_card.follow')
|
||||
|
@ -33,20 +33,20 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
this.user.following ? this.unfollow() : this.follow()
|
||||
this.relationship.following ? this.unfollow() : this.follow()
|
||||
},
|
||||
follow () {
|
||||
this.inProgress = true
|
||||
requestFollow(this.user, this.$store).then(() => {
|
||||
requestFollow(this.relationship.id, this.$store).then(() => {
|
||||
this.inProgress = false
|
||||
})
|
||||
},
|
||||
unfollow () {
|
||||
const store = this.$store
|
||||
this.inProgress = true
|
||||
requestUnfollow(this.user, store).then(() => {
|
||||
requestUnfollow(this.relationship.id, store).then(() => {
|
||||
this.inProgress = false
|
||||
store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
|
||||
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<button
|
||||
class="btn btn-default follow-button"
|
||||
:class="{ pressed: isPressed }"
|
||||
:class="{ toggled: isPressed }"
|
||||
:disabled="inProgress"
|
||||
:title="title"
|
||||
@click="onClick"
|
||||
|
|
|
@ -18,6 +18,9 @@ const FollowCard = {
|
|||
},
|
||||
loggedIn () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
relationship () {
|
||||
return this.$store.getters.relationship(this.user.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,24 +2,24 @@
|
|||
<basic-user-card :user="user">
|
||||
<div class="follow-card-content-container">
|
||||
<span
|
||||
v-if="!noFollowsYou && user.follows_you"
|
||||
v-if="isMe || (!noFollowsYou && relationship.followed_by)"
|
||||
class="faint"
|
||||
>
|
||||
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
||||
</span>
|
||||
<template v-if="!loggedIn">
|
||||
<div
|
||||
v-if="!user.following"
|
||||
v-if="!relationship.following"
|
||||
class="follow-card-follow-button"
|
||||
>
|
||||
<RemoteFollow :user="user" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-else-if="!isMe">
|
||||
<FollowButton
|
||||
:user="user"
|
||||
class="follow-card-follow-button"
|
||||
:relationship="relationship"
|
||||
:label-following="$t('user_card.follow_unfollow')"
|
||||
class="follow-card-follow-button"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js'
|
||||
|
||||
const FollowRequestCard = {
|
||||
props: ['user'],
|
||||
|
@ -6,13 +7,32 @@ const FollowRequestCard = {
|
|||
BasicUserCard
|
||||
},
|
||||
methods: {
|
||||
findFollowRequestNotificationId () {
|
||||
const notif = notificationsFromStore(this.$store).find(
|
||||
(notif) => notif.from_profile.id === this.user.id && notif.type === 'follow_request'
|
||||
)
|
||||
return notif && notif.id
|
||||
},
|
||||
approveUser () {
|
||||
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
|
||||
const notifId = this.findFollowRequestNotificationId()
|
||||
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
|
||||
this.$store.dispatch('updateNotification', {
|
||||
id: notifId,
|
||||
updater: notification => {
|
||||
notification.type = 'follow'
|
||||
}
|
||||
})
|
||||
},
|
||||
denyUser () {
|
||||
const notifId = this.findFollowRequestNotificationId()
|
||||
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
|
||||
.then(() => {
|
||||
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
video,
|
||||
canvas {
|
||||
object-fit: contain;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ import _ from 'lodash'
|
|||
export default {
|
||||
computed: {
|
||||
languageCodes () {
|
||||
return Object.keys(languagesObject)
|
||||
return languagesObject.languages
|
||||
},
|
||||
|
||||
languageNames () {
|
||||
|
@ -43,7 +43,6 @@ export default {
|
|||
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
|
||||
set: function (val) {
|
||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
this.$i18n.locale = val
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -84,10 +84,12 @@ const MediaModal = {
|
|||
}
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('popstate', this.hide)
|
||||
document.addEventListener('keyup', this.handleKeyupEvent)
|
||||
document.addEventListener('keydown', this.handleKeydownEvent)
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('popstate', this.hide)
|
||||
document.removeEventListener('keyup', this.handleKeyupEvent)
|
||||
document.removeEventListener('keydown', this.handleKeydownEvent)
|
||||
}
|
||||
|
|
|
@ -5,10 +5,15 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for
|
|||
const mediaUpload = {
|
||||
data () {
|
||||
return {
|
||||
uploading: false,
|
||||
uploadCount: 0,
|
||||
uploadReady: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
uploading () {
|
||||
return this.uploadCount > 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
uploadFile (file) {
|
||||
const self = this
|
||||
|
@ -23,29 +28,21 @@ const mediaUpload = {
|
|||
formData.append('file', file)
|
||||
|
||||
self.$emit('uploading')
|
||||
self.uploading = true
|
||||
self.uploadCount++
|
||||
|
||||
statusPosterService.uploadMedia({ store, formData })
|
||||
.then((fileData) => {
|
||||
self.$emit('uploaded', fileData)
|
||||
self.uploading = false
|
||||
self.decreaseUploadCount()
|
||||
}, (error) => { // eslint-disable-line handle-callback-err
|
||||
self.$emit('upload-failed', 'default')
|
||||
self.uploading = false
|
||||
self.decreaseUploadCount()
|
||||
})
|
||||
},
|
||||
fileDrop (e) {
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
e.preventDefault() // allow dropping text like before
|
||||
this.uploadFile(e.dataTransfer.files[0])
|
||||
}
|
||||
},
|
||||
fileDrag (e) {
|
||||
let types = e.dataTransfer.types
|
||||
if (types.contains('Files')) {
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
} else {
|
||||
e.dataTransfer.dropEffect = 'none'
|
||||
decreaseUploadCount () {
|
||||
this.uploadCount--
|
||||
if (this.uploadCount === 0) {
|
||||
this.$emit('all-uploaded')
|
||||
}
|
||||
},
|
||||
clearFile () {
|
||||
|
@ -54,11 +51,13 @@ const mediaUpload = {
|
|||
this.uploadReady = true
|
||||
})
|
||||
},
|
||||
change ({ target }) {
|
||||
for (var i = 0; i < target.files.length; i++) {
|
||||
let file = target.files[i]
|
||||
multiUpload (files) {
|
||||
for (const file of files) {
|
||||
this.uploadFile(file)
|
||||
}
|
||||
},
|
||||
change ({ target }) {
|
||||
this.multiUpload(target.files)
|
||||
}
|
||||
},
|
||||
props: [
|
||||
|
@ -67,7 +66,7 @@ const mediaUpload = {
|
|||
watch: {
|
||||
'dropFiles': function (fileInfos) {
|
||||
if (!this.uploading) {
|
||||
this.uploadFile(fileInfos[0])
|
||||
this.multiUpload(fileInfos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
<template>
|
||||
<div
|
||||
class="media-upload"
|
||||
@drop.prevent
|
||||
@dragover.prevent="fileDrag"
|
||||
@drop="fileDrop"
|
||||
>
|
||||
<div class="media-upload">
|
||||
<label
|
||||
class="label"
|
||||
:title="$t('tool_tip.media_upload')"
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<template>
|
||||
<div
|
||||
v-show="isOpen"
|
||||
v-body-scroll-lock="isOpen"
|
||||
v-body-scroll-lock="isOpen && !noBackground"
|
||||
class="modal-view"
|
||||
:class="classes"
|
||||
@click.self="$emit('backdropClicked')"
|
||||
>
|
||||
<slot />
|
||||
|
@ -15,6 +16,18 @@ export default {
|
|||
isOpen: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
noBackground: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return {
|
||||
'modal-background': !this.noBackground,
|
||||
'open': this.isOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,12 +45,22 @@ export default {
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
pointer-events: none;
|
||||
animation-duration: 0.2s;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
animation-name: modal-background-fadein;
|
||||
|
||||
body:not(.scroll-locked) & {
|
||||
opacity: 0;
|
||||
|
||||
> * {
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
&.modal-background {
|
||||
pointer-events: initial;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
&.open {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import DialogModal from '../dialog_modal/dialog_modal.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
|
||||
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
|
||||
const STRIP_MEDIA = 'mrf_tag:media-strip'
|
||||
|
@ -14,7 +15,6 @@ const ModerationTools = {
|
|||
],
|
||||
data () {
|
||||
return {
|
||||
showDropDown: false,
|
||||
tags: {
|
||||
FORCE_NSFW,
|
||||
STRIP_MEDIA,
|
||||
|
@ -24,11 +24,13 @@ const ModerationTools = {
|
|||
SANDBOX,
|
||||
QUARANTINE
|
||||
},
|
||||
showDeleteUserDialog: false
|
||||
showDeleteUserDialog: false,
|
||||
toggled: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
DialogModal
|
||||
DialogModal,
|
||||
Popover
|
||||
},
|
||||
computed: {
|
||||
tagsSet () {
|
||||
|
@ -89,6 +91,9 @@ const ModerationTools = {
|
|||
window.history.back()
|
||||
}
|
||||
})
|
||||
},
|
||||
setToggled (value) {
|
||||
this.toggled = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-popover
|
||||
<Popover
|
||||
trigger="click"
|
||||
class="moderation-tools-popover"
|
||||
placement="bottom-end"
|
||||
@show="showDropDown = true"
|
||||
@hide="showDropDown = false"
|
||||
placement="bottom"
|
||||
:offset="{ y: 5 }"
|
||||
@show="setToggled(true)"
|
||||
@close="setToggled(false)"
|
||||
>
|
||||
<div slot="popover">
|
||||
<div slot="content">
|
||||
<div class="dropdown-menu">
|
||||
<span v-if="user.is_local">
|
||||
<button
|
||||
|
@ -122,12 +123,13 @@
|
|||
</div>
|
||||
</div>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="btn btn-default btn-block"
|
||||
:class="{ pressed: showDropDown }"
|
||||
:class="{ toggled }"
|
||||
>
|
||||
{{ $t('user_card.admin_menu.moderation') }}
|
||||
</button>
|
||||
</v-popover>
|
||||
</Popover>
|
||||
<portal to="modal">
|
||||
<DialogModal
|
||||
v-if="showDeleteUserDialog"
|
||||
|
@ -160,7 +162,6 @@
|
|||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../popper/popper.scss';
|
||||
|
||||
.menu-checkbox {
|
||||
float: right;
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
<div class="panel panel-default base01-background">
|
||||
<div class="panel-heading timeline-heading base02-background">
|
||||
<div class="title">
|
||||
{{ $t("about.federation") }}
|
||||
{{ $t("about.mrf.federation") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="mrf-section">
|
||||
<h2>{{ $t("about.mrf_policies") }}</h2>
|
||||
<p>{{ $t("about.mrf_policies_desc") }}</p>
|
||||
<h2>{{ $t("about.mrf.mrf_policies") }}</h2>
|
||||
<p>{{ $t("about.mrf.mrf_policies_desc") }}</p>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
|
@ -23,13 +23,13 @@
|
|||
</ul>
|
||||
|
||||
<h2 v-if="hasInstanceSpecificPolicies">
|
||||
{{ $t("about.mrf_policy_simple") }}
|
||||
{{ $t("about.mrf.simple.simple_policies") }}
|
||||
</h2>
|
||||
|
||||
<div v-if="acceptInstances.length">
|
||||
<h4>{{ $t("about.mrf_policy_simple_accept") }}</h4>
|
||||
<h4>{{ $t("about.mrf.simple.accept") }}</h4>
|
||||
|
||||
<p>{{ $t("about.mrf_policy_simple_accept_desc") }}</p>
|
||||
<p>{{ $t("about.mrf.simple.accept_desc") }}</p>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
|
@ -41,9 +41,9 @@
|
|||
</div>
|
||||
|
||||
<div v-if="rejectInstances.length">
|
||||
<h4>{{ $t("about.mrf_policy_simple_reject") }}</h4>
|
||||
<h4>{{ $t("about.mrf.simple.reject") }}</h4>
|
||||
|
||||
<p>{{ $t("about.mrf_policy_simple_reject_desc") }}</p>
|
||||
<p>{{ $t("about.mrf.simple.reject_desc") }}</p>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
|
@ -55,9 +55,9 @@
|
|||
</div>
|
||||
|
||||
<div v-if="quarantineInstances.length">
|
||||
<h4>{{ $t("about.mrf_policy_simple_quarantine") }}</h4>
|
||||
<h4>{{ $t("about.mrf.simple.quarantine") }}</h4>
|
||||
|
||||
<p>{{ $t("about.mrf_policy_simple_quarantine_desc") }}</p>
|
||||
<p>{{ $t("about.mrf.simple.quarantine_desc") }}</p>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
|
@ -69,9 +69,9 @@
|
|||
</div>
|
||||
|
||||
<div v-if="ftlRemovalInstances.length">
|
||||
<h4>{{ $t("about.mrf_policy_simple_ftl_removal") }}</h4>
|
||||
<h4>{{ $t("about.mrf.simple.ftl_removal") }}</h4>
|
||||
|
||||
<p>{{ $t("about.mrf_policy_simple_ftl_removal_desc") }}</p>
|
||||
<p>{{ $t("about.mrf.simple.ftl_removal_desc") }}</p>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
|
@ -83,9 +83,9 @@
|
|||
</div>
|
||||
|
||||
<div v-if="mediaNsfwInstances.length">
|
||||
<h4>{{ $t("about.mrf_policy_simple_media_nsfw") }}</h4>
|
||||
<h4>{{ $t("about.mrf.simple.media_nsfw") }}</h4>
|
||||
|
||||
<p>{{ $t("about.mrf_policy_simple_media_nsfw_desc") }}</p>
|
||||
<p>{{ $t("about.mrf.simple.media_nsfw_desc") }}</p>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
|
@ -97,9 +97,9 @@
|
|||
</div>
|
||||
|
||||
<div v-if="mediaRemovalInstances.length">
|
||||
<h4>{{ $t("about.mrf_policy_simple_media_removal") }}</h4>
|
||||
<h4>{{ $t("about.mrf.simple.media_removal") }}</h4>
|
||||
|
||||
<p>{{ $t("about.mrf_policy_simple_media_removal_desc") }}</p>
|
||||
<p>{{ $t("about.mrf.simple.media_removal_desc") }}</p>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
|
|
|
@ -11,8 +11,11 @@ const MuteCard = {
|
|||
user () {
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
relationship () {
|
||||
return this.$store.getters.relationship(this.userId)
|
||||
},
|
||||
muted () {
|
||||
return this.user.muted
|
||||
return this.relationship.muting
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -21,13 +24,13 @@ const MuteCard = {
|
|||
methods: {
|
||||
unmuteUser () {
|
||||
this.progress = true
|
||||
this.$store.dispatch('unmuteUser', this.user.id).then(() => {
|
||||
this.$store.dispatch('unmuteUser', this.userId).then(() => {
|
||||
this.progress = false
|
||||
})
|
||||
},
|
||||
muteUser () {
|
||||
this.progress = true
|
||||
this.$store.dispatch('muteUser', this.user.id).then(() => {
|
||||
this.$store.dispatch('muteUser', this.userId).then(() => {
|
||||
this.progress = false
|
||||
})
|
||||
}
|
||||
|
|
|
@ -100,13 +100,25 @@
|
|||
|
||||
&:hover {
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||
color: $fallback--link;
|
||||
color: var(--selectedMenuText, $fallback--link);
|
||||
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuIcon, $fallback--icon);
|
||||
}
|
||||
|
||||
&.router-link-active {
|
||||
font-weight: bolder;
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||
color: $fallback--text;
|
||||
color: var(--selectedMenuText, $fallback--text);
|
||||
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuIcon, $fallback--icon);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import StatusContent from '../status_content/status_content.vue'
|
||||
import Status from '../status/status.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import Timeago from '../timeago/timeago.vue'
|
||||
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
|
@ -15,10 +17,11 @@ const Notification = {
|
|||
},
|
||||
props: [ 'notification' ],
|
||||
components: {
|
||||
Status,
|
||||
StatusContent,
|
||||
UserAvatar,
|
||||
UserCard,
|
||||
Timeago
|
||||
Timeago,
|
||||
Status
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
|
@ -32,6 +35,24 @@ const Notification = {
|
|||
},
|
||||
toggleMute () {
|
||||
this.unmuted = !this.unmuted
|
||||
},
|
||||
approveUser () {
|
||||
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id })
|
||||
this.$store.dispatch('updateNotification', {
|
||||
id: this.notification.id,
|
||||
updater: notification => {
|
||||
notification.type = 'follow'
|
||||
}
|
||||
})
|
||||
},
|
||||
denyUser () {
|
||||
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
|
||||
.then(() => {
|
||||
this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id })
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -56,7 +77,10 @@ const Notification = {
|
|||
return this.generateUserProfileLink(this.targetUser)
|
||||
},
|
||||
needMute () {
|
||||
return this.user.muted
|
||||
return this.$store.getters.relationship(this.user.id).muting
|
||||
},
|
||||
isStatusNotification () {
|
||||
return isStatusNotification(this.notification.type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,14 +40,14 @@
|
|||
<div class="notification-right">
|
||||
<UserCard
|
||||
v-if="userExpanded"
|
||||
:user="getUser(notification)"
|
||||
:user-id="getUser(notification).id"
|
||||
:rounded="true"
|
||||
:bordered="true"
|
||||
/>
|
||||
<span class="notification-details">
|
||||
<div class="name-and-action">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
<bdi
|
||||
v-if="!!notification.from_profile.name_html"
|
||||
class="username"
|
||||
:title="'@'+notification.from_profile.screen_name"
|
||||
|
@ -74,6 +74,10 @@
|
|||
<i class="fa icon-user-plus lit" />
|
||||
<small>{{ $t('notifications.followed_you') }}</small>
|
||||
</span>
|
||||
<span v-if="notification.type === 'follow_request'">
|
||||
<i class="fa icon-user lit" />
|
||||
<small>{{ $t('notifications.follow_request') }}</small>
|
||||
</span>
|
||||
<span v-if="notification.type === 'move'">
|
||||
<i class="fa icon-arrow-curved lit" />
|
||||
<small>{{ $t('notifications.migrated_to') }}</small>
|
||||
|
@ -87,18 +91,7 @@
|
|||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="notification.type === 'follow' || notification.type === 'move'"
|
||||
class="timeago"
|
||||
>
|
||||
<span class="faint">
|
||||
<Timeago
|
||||
:time="notification.created_at"
|
||||
:auto-update="240"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
v-if="isStatusNotification"
|
||||
class="timeago"
|
||||
>
|
||||
<router-link
|
||||
|
@ -112,6 +105,17 @@
|
|||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="timeago"
|
||||
>
|
||||
<span class="faint">
|
||||
<Timeago
|
||||
:time="notification.created_at"
|
||||
:auto-update="240"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<a
|
||||
v-if="needMute"
|
||||
href="#"
|
||||
|
@ -119,12 +123,30 @@
|
|||
><i class="button-icon icon-eye-off" /></a>
|
||||
</span>
|
||||
<div
|
||||
v-if="notification.type === 'follow'"
|
||||
v-if="notification.type === 'follow' || notification.type === 'follow_request'"
|
||||
class="follow-text"
|
||||
>
|
||||
<router-link :to="userProfileLink">
|
||||
<router-link
|
||||
:to="userProfileLink"
|
||||
class="follow-name"
|
||||
>
|
||||
@{{ notification.from_profile.screen_name }}
|
||||
</router-link>
|
||||
<div
|
||||
v-if="notification.type === 'follow_request'"
|
||||
style="white-space: nowrap;"
|
||||
>
|
||||
<i
|
||||
class="icon-ok button-icon follow-request-accept"
|
||||
:title="$t('tool_tip.accept_follow_request')"
|
||||
@click="approveUser()"
|
||||
/>
|
||||
<i
|
||||
class="icon-cancel button-icon follow-request-reject"
|
||||
:title="$t('tool_tip.reject_follow_request')"
|
||||
@click="denyUser()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="notification.type === 'move'"
|
||||
|
@ -135,11 +157,9 @@
|
|||
</router-link>
|
||||
</div>
|
||||
<template v-else>
|
||||
<status
|
||||
<status-content
|
||||
class="faint"
|
||||
:compact="true"
|
||||
:statusoid="notification.action"
|
||||
:no-heading="true"
|
||||
:status="notification.action"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -36,6 +36,8 @@
|
|||
border-bottom: 1px solid;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
|
||||
&:hover .animated.avatar {
|
||||
canvas {
|
||||
|
@ -46,38 +48,62 @@
|
|||
}
|
||||
}
|
||||
|
||||
.muted {
|
||||
padding: .25em .6em;
|
||||
}
|
||||
|
||||
.non-mention {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: nowrap;
|
||||
padding: 0.6em;
|
||||
min-width: 0;
|
||||
|
||||
.avatar-container {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.status-el {
|
||||
.status {
|
||||
padding: 0.25em 0;
|
||||
|
||||
.status-body {
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
a {
|
||||
color: var(--faintLink);
|
||||
}
|
||||
}
|
||||
padding: 0;
|
||||
.media-body {
|
||||
margin: 0;
|
||||
.status-content a {
|
||||
color: var(--postFaintLink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.follow-request-accept {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
.follow-request-reject {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $fallback--cRed;
|
||||
color: var(--cRed, $fallback--cRed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.follow-text, .move-text {
|
||||
padding: 0.5em 0;
|
||||
overflow-wrap: break-word;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.follow-name {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.status-el {
|
||||
|
@ -139,6 +165,11 @@
|
|||
color: var(--cGreen, $fallback--cGreen);
|
||||
}
|
||||
|
||||
.icon-user.lit {
|
||||
color: $fallback--cBlue;
|
||||
color: var(--cBlue, $fallback--cBlue);
|
||||
}
|
||||
|
||||
.icon-user-plus.lit {
|
||||
color: $fallback--cBlue;
|
||||
color: var(--cBlue, $fallback--cBlue);
|
||||
|
|
|
@ -9,18 +9,12 @@
|
|||
>
|
||||
{{ $t('settings.style.common.opacity') }}
|
||||
</label>
|
||||
<input
|
||||
<Checkbox
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
:id="name + '-o'"
|
||||
class="opt exclude-disabled"
|
||||
type="checkbox"
|
||||
:checked="present"
|
||||
@input="$emit('input', !present ? fallback : undefined)"
|
||||
>
|
||||
<label
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
class="opt-l"
|
||||
:for="name + '-o'"
|
||||
:disabled="disabled"
|
||||
class="opt"
|
||||
@change="$emit('input', !present ? fallback : undefined)"
|
||||
/>
|
||||
<input
|
||||
:id="name"
|
||||
|
@ -37,7 +31,11 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
export default {
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
props: [
|
||||
'name', 'value', 'fallback', 'disabled'
|
||||
],
|
||||
|
|
29
src/components/panel_loading/panel_loading.vue
Normal file
29
src/components/panel_loading/panel_loading.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="panel-loading">
|
||||
<span class="loading-text">
|
||||
<i class="icon-spin4 animate-spin" />
|
||||
{{ $t('general.loading') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'src/_variables.scss';
|
||||
|
||||
.panel-loading {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2em;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
.loading-text i {
|
||||
font-size: 3em;
|
||||
line-height: 0;
|
||||
vertical-align: middle;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -104,8 +104,10 @@
|
|||
.result-fill {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
color: $fallback--text;
|
||||
color: var(--pollText, $fallback--text);
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--linkBg, $fallback--lightBg);
|
||||
background-color: var(--poll, $fallback--lightBg);
|
||||
border-radius: $fallback--panelRadius;
|
||||
border-radius: var(--panelRadius, $fallback--panelRadius);
|
||||
top: 0;
|
||||
|
|
156
src/components/popover/popover.js
Normal file
156
src/components/popover/popover.js
Normal file
|
@ -0,0 +1,156 @@
|
|||
|
||||
const Popover = {
|
||||
name: 'Popover',
|
||||
props: {
|
||||
// Action to trigger popover: either 'hover' or 'click'
|
||||
trigger: String,
|
||||
// Either 'top' or 'bottom'
|
||||
placement: String,
|
||||
// Takes object with properties 'x' and 'y', values of these can be
|
||||
// 'container' for using offsetParent as boundaries for either axis
|
||||
// or 'viewport'
|
||||
boundTo: Object,
|
||||
// Takes a top/bottom/left/right object, how much space to leave
|
||||
// between boundary and popover element
|
||||
margin: Object,
|
||||
// Takes a x/y object and tells how many pixels to offset from
|
||||
// anchor point on either axis
|
||||
offset: Object,
|
||||
// Additional styles you may want for the popover container
|
||||
popoverClass: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
hidden: true,
|
||||
styles: { opacity: 0 },
|
||||
oldSize: { width: 0, height: 0 }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateStyles () {
|
||||
if (this.hidden) {
|
||||
this.styles = {
|
||||
opacity: 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Popover will be anchored around this element, trigger ref is the container, so
|
||||
// its children are what are inside the slot. Expect only one slot="trigger".
|
||||
const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
|
||||
const screenBox = anchorEl.getBoundingClientRect()
|
||||
// Screen position of the origin point for popover
|
||||
const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }
|
||||
const content = this.$refs.content
|
||||
// Minor optimization, don't call a slow reflow call if we don't have to
|
||||
const parentBounds = this.boundTo &&
|
||||
(this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
|
||||
this.$el.offsetParent.getBoundingClientRect()
|
||||
const margin = this.margin || {}
|
||||
|
||||
// What are the screen bounds for the popover? Viewport vs container
|
||||
// when using viewport, using default margin values to dodge the navbar
|
||||
const xBounds = this.boundTo && this.boundTo.x === 'container' ? {
|
||||
min: parentBounds.left + (margin.left || 0),
|
||||
max: parentBounds.right - (margin.right || 0)
|
||||
} : {
|
||||
min: 0 + (margin.left || 10),
|
||||
max: window.innerWidth - (margin.right || 10)
|
||||
}
|
||||
|
||||
const yBounds = this.boundTo && this.boundTo.y === 'container' ? {
|
||||
min: parentBounds.top + (margin.top || 0),
|
||||
max: parentBounds.bottom - (margin.bottom || 0)
|
||||
} : {
|
||||
min: 0 + (margin.top || 50),
|
||||
max: window.innerHeight - (margin.bottom || 5)
|
||||
}
|
||||
|
||||
let horizOffset = 0
|
||||
|
||||
// If overflowing from left, move it so that it doesn't
|
||||
if ((origin.x - content.offsetWidth * 0.5) < xBounds.min) {
|
||||
horizOffset += -(origin.x - content.offsetWidth * 0.5) + xBounds.min
|
||||
}
|
||||
|
||||
// If overflowing from right, move it so that it doesn't
|
||||
if ((origin.x + horizOffset + content.offsetWidth * 0.5) > xBounds.max) {
|
||||
horizOffset -= (origin.x + horizOffset + content.offsetWidth * 0.5) - xBounds.max
|
||||
}
|
||||
|
||||
// Default to whatever user wished with placement prop
|
||||
let usingTop = this.placement !== 'bottom'
|
||||
|
||||
// Handle special cases, first force to displaying on top if there's not space on bottom,
|
||||
// regardless of what placement value was. Then check if there's not space on top, and
|
||||
// force to bottom, again regardless of what placement value was.
|
||||
if (origin.y + content.offsetHeight > yBounds.max) usingTop = true
|
||||
if (origin.y - content.offsetHeight < yBounds.min) usingTop = false
|
||||
|
||||
const yOffset = (this.offset && this.offset.y) || 0
|
||||
const translateY = usingTop
|
||||
? -anchorEl.offsetHeight - yOffset - content.offsetHeight
|
||||
: yOffset
|
||||
|
||||
const xOffset = (this.offset && this.offset.x) || 0
|
||||
const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset
|
||||
|
||||
// Note, separate translateX and translateY avoids blurry text on chromium,
|
||||
// single translate or translate3d resulted in blurry text.
|
||||
this.styles = {
|
||||
opacity: 1,
|
||||
transform: `translateX(${Math.floor(translateX)}px) translateY(${Math.floor(translateY)}px)`
|
||||
}
|
||||
},
|
||||
showPopover () {
|
||||
if (this.hidden) this.$emit('show')
|
||||
this.hidden = false
|
||||
this.$nextTick(this.updateStyles)
|
||||
},
|
||||
hidePopover () {
|
||||
if (!this.hidden) this.$emit('close')
|
||||
this.hidden = true
|
||||
this.styles = { opacity: 0 }
|
||||
},
|
||||
onMouseenter (e) {
|
||||
if (this.trigger === 'hover') this.showPopover()
|
||||
},
|
||||
onMouseleave (e) {
|
||||
if (this.trigger === 'hover') this.hidePopover()
|
||||
},
|
||||
onClick (e) {
|
||||
if (this.trigger === 'click') {
|
||||
if (this.hidden) {
|
||||
this.showPopover()
|
||||
} else {
|
||||
this.hidePopover()
|
||||
}
|
||||
}
|
||||
},
|
||||
onClickOutside (e) {
|
||||
if (this.hidden) return
|
||||
if (this.$el.contains(e.target)) return
|
||||
this.hidePopover()
|
||||
}
|
||||
},
|
||||
updated () {
|
||||
// Monitor changes to content size, update styles only when content sizes have changed,
|
||||
// that should be the only time we need to move the popover box if we don't care about scroll
|
||||
// or resize
|
||||
const content = this.$refs.content
|
||||
if (!content) return
|
||||
if (this.oldSize.width !== content.offsetWidth || this.oldSize.height !== content.offsetHeight) {
|
||||
this.updateStyles()
|
||||
this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
|
||||
}
|
||||
},
|
||||
created () {
|
||||
document.addEventListener('click', this.onClickOutside)
|
||||
},
|
||||
destroyed () {
|
||||
document.removeEventListener('click', this.onClickOutside)
|
||||
this.hidePopover()
|
||||
}
|
||||
}
|
||||
|
||||
export default Popover
|
118
src/components/popover/popover.vue
Normal file
118
src/components/popover/popover.vue
Normal file
|
@ -0,0 +1,118 @@
|
|||
<template>
|
||||
<div
|
||||
@mouseenter="onMouseenter"
|
||||
@mouseleave="onMouseleave"
|
||||
>
|
||||
<div
|
||||
ref="trigger"
|
||||
@click="onClick"
|
||||
>
|
||||
<slot name="trigger" />
|
||||
</div>
|
||||
<div
|
||||
v-if="!hidden"
|
||||
ref="content"
|
||||
:style="styles"
|
||||
class="popover"
|
||||
:class="popoverClass"
|
||||
>
|
||||
<slot
|
||||
name="content"
|
||||
class="popover-inner"
|
||||
:close="hidePopover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./popover.js" />
|
||||
|
||||
<style lang=scss>
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.popover {
|
||||
z-index: 8;
|
||||
position: absolute;
|
||||
min-width: 0;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
|
||||
box-shadow: var(--panelShadow);
|
||||
border-radius: $fallback--btnRadius;
|
||||
border-radius: var(--btnRadius, $fallback--btnRadius);
|
||||
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--popover, $fallback--bg);
|
||||
color: $fallback--text;
|
||||
color: var(--popoverText, $fallback--text);
|
||||
--faint: var(--popoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||
--postLink: var(--popoverPostLink, $fallback--link);
|
||||
--postFaintLink: var(--popoverPostFaintLink, $fallback--link);
|
||||
--icon: var(--popoverIcon, $fallback--icon);
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
display: block;
|
||||
padding: .5rem 0;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
list-style: none;
|
||||
max-width: 100vw;
|
||||
z-index: 10;
|
||||
white-space: nowrap;
|
||||
|
||||
.dropdown-divider {
|
||||
height: 0;
|
||||
margin: .5rem 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid $fallback--border;
|
||||
border-top: 1px solid var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
line-height: 21px;
|
||||
margin-right: 5px;
|
||||
overflow: auto;
|
||||
display: block;
|
||||
padding: .25rem 1.0rem .25rem 1.5rem;
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
text-align: inherit;
|
||||
white-space: nowrap;
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
--btnText: var(--popoverText, $fallback--text);
|
||||
|
||||
&-icon {
|
||||
padding-left: 0.5rem;
|
||||
|
||||
i {
|
||||
margin-right: 0.25rem;
|
||||
color: var(--menuPopoverIcon, $fallback--icon)
|
||||
}
|
||||
}
|
||||
|
||||
&:active, &:hover {
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--selectedMenuPopover, $fallback--lightBg);
|
||||
color: $fallback--link;
|
||||
color: var(--selectedMenuPopoverText, $fallback--link);
|
||||
--faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||
i {
|
||||
color: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,147 +0,0 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.tooltip.popover {
|
||||
z-index: 8;
|
||||
|
||||
.popover-inner {
|
||||
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
|
||||
box-shadow: var(--panelShadow);
|
||||
border-radius: $fallback--btnRadius;
|
||||
border-radius: var(--btnRadius, $fallback--btnRadius);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
}
|
||||
|
||||
.popover-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
position: absolute;
|
||||
margin: 5px;
|
||||
border-color: $fallback--bg;
|
||||
border-color: var(--bg, $fallback--bg);
|
||||
}
|
||||
|
||||
&[x-placement^="top"] {
|
||||
margin-bottom: 5px;
|
||||
|
||||
.popover-arrow {
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-left-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
bottom: -4px;
|
||||
left: calc(50% - 5px);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&[x-placement^="bottom"] {
|
||||
margin-top: 5px;
|
||||
|
||||
.popover-arrow {
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-left-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-top-color: transparent !important;
|
||||
top: -4px;
|
||||
left: calc(50% - 5px);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&[x-placement^="right"] {
|
||||
margin-left: 5px;
|
||||
|
||||
.popover-arrow {
|
||||
border-width: 5px 5px 5px 0;
|
||||
border-left-color: transparent !important;
|
||||
border-top-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
left: -4px;
|
||||
top: calc(50% - 5px);
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&[x-placement^="left"] {
|
||||
margin-right: 5px;
|
||||
|
||||
.popover-arrow {
|
||||
border-width: 5px 0 5px 5px;
|
||||
border-top-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
right: -4px;
|
||||
top: calc(50% - 5px);
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&[aria-hidden='true'] {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity .15s, visibility .15s;
|
||||
}
|
||||
|
||||
&[aria-hidden='false'] {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
display: block;
|
||||
padding: .5rem 0;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
list-style: none;
|
||||
max-width: 100vw;
|
||||
z-index: 10;
|
||||
|
||||
.dropdown-divider {
|
||||
height: 0;
|
||||
margin: .5rem 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid $fallback--border;
|
||||
border-top: 1px solid var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
line-height: 21px;
|
||||
margin-right: 5px;
|
||||
overflow: auto;
|
||||
display: block;
|
||||
padding: .25rem 1.0rem .25rem 1.5rem;
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
text-align: inherit;
|
||||
white-space: normal;
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&-icon {
|
||||
padding-left: 0.5rem;
|
||||
|
||||
i {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
// TODO: improve the look on breeze themes
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btn, $fallback--fg);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -82,7 +82,9 @@ const PostStatusForm = {
|
|||
contentType
|
||||
},
|
||||
caret: 0,
|
||||
pollFormVisible: false
|
||||
pollFormVisible: false,
|
||||
showDropIcon: 'hide',
|
||||
dropStopTimeout: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -102,7 +104,7 @@ const PostStatusForm = {
|
|||
...this.$store.state.instance.customEmoji
|
||||
],
|
||||
users: this.$store.state.users.users,
|
||||
updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
|
||||
updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
|
||||
})
|
||||
},
|
||||
emojiSuggestor () {
|
||||
|
@ -218,7 +220,6 @@ const PostStatusForm = {
|
|||
},
|
||||
addMediaFile (fileInfo) {
|
||||
this.newStatus.files.push(fileInfo)
|
||||
this.enableSubmit()
|
||||
},
|
||||
removeMediaFile (fileInfo) {
|
||||
let index = this.newStatus.files.indexOf(fileInfo)
|
||||
|
@ -227,7 +228,6 @@ const PostStatusForm = {
|
|||
uploadFailed (errString, templateArgs) {
|
||||
templateArgs = templateArgs || {}
|
||||
this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs)
|
||||
this.enableSubmit()
|
||||
},
|
||||
disableSubmit () {
|
||||
this.submitDisabled = true
|
||||
|
@ -250,13 +250,27 @@ const PostStatusForm = {
|
|||
}
|
||||
},
|
||||
fileDrop (e) {
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
|
||||
e.preventDefault() // allow dropping text like before
|
||||
this.dropFiles = e.dataTransfer.files
|
||||
clearTimeout(this.dropStopTimeout)
|
||||
this.showDropIcon = 'hide'
|
||||
}
|
||||
},
|
||||
fileDragStop (e) {
|
||||
// The false-setting is done with delay because just using leave-events
|
||||
// directly caused unwanted flickering, this is not perfect either but
|
||||
// much less noticable.
|
||||
clearTimeout(this.dropStopTimeout)
|
||||
this.showDropIcon = 'fade'
|
||||
this.dropStopTimeout = setTimeout(() => (this.showDropIcon = 'hide'), 500)
|
||||
},
|
||||
fileDrag (e) {
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
|
||||
clearTimeout(this.dropStopTimeout)
|
||||
this.showDropIcon = 'show'
|
||||
}
|
||||
},
|
||||
onEmojiInputInput (e) {
|
||||
this.$nextTick(() => {
|
||||
|
|
|
@ -6,7 +6,15 @@
|
|||
<form
|
||||
autocomplete="off"
|
||||
@submit.prevent="postStatus(newStatus)"
|
||||
@dragover.prevent="fileDrag"
|
||||
>
|
||||
<div
|
||||
v-show="showDropIcon !== 'hide'"
|
||||
:style="{ animation: showDropIcon === 'show' ? 'fade-in 0.25s' : 'fade-out 0.5s' }"
|
||||
class="drop-indicator icon-upload"
|
||||
@dragleave="fileDragStop"
|
||||
@drop.stop="fileDrop"
|
||||
/>
|
||||
<div class="form-group">
|
||||
<i18n
|
||||
v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'"
|
||||
|
@ -73,6 +81,7 @@
|
|||
v-model="newStatus.spoilerText"
|
||||
type="text"
|
||||
:placeholder="$t('post_status.content_warning')"
|
||||
:disabled="posting"
|
||||
class="form-post-subject"
|
||||
>
|
||||
</EmojiInput>
|
||||
|
@ -96,9 +105,7 @@
|
|||
:disabled="posting"
|
||||
class="form-post-body"
|
||||
@keydown.meta.enter="postStatus(newStatus)"
|
||||
@keyup.ctrl.enter="postStatus(newStatus)"
|
||||
@drop="fileDrop"
|
||||
@dragover.prevent="fileDrag"
|
||||
@keydown.ctrl.enter="postStatus(newStatus)"
|
||||
@input="resize"
|
||||
@compositionupdate="resize"
|
||||
@paste="paste"
|
||||
|
@ -172,6 +179,7 @@
|
|||
@uploading="disableSubmit"
|
||||
@uploaded="addMediaFile"
|
||||
@upload-failed="uploadFailed"
|
||||
@all-uploaded="enableSubmit"
|
||||
/>
|
||||
<div
|
||||
class="emoji-icon"
|
||||
|
@ -446,7 +454,8 @@
|
|||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.6em;
|
||||
margin: 0.6em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
|
@ -504,5 +513,35 @@
|
|||
cursor: pointer;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 0.6; }
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from { opacity: 0.6; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
.drop-indicator {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.6;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
border-radius: $fallback--tooltipRadius;
|
||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||
border: 2px dashed $fallback--text;
|
||||
border: 2px dashed var(--text, $fallback--text);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<input
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
:id="name + '-o'"
|
||||
class="opt exclude-disabled"
|
||||
class="opt"
|
||||
type="checkbox"
|
||||
:checked="present"
|
||||
@input="$emit('input', !present ? fallback : undefined)"
|
||||
|
|
|
@ -1,39 +1,30 @@
|
|||
import Popover from '../popover/popover.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
const ReactButton = {
|
||||
props: ['status', 'loggedIn'],
|
||||
props: ['status'],
|
||||
data () {
|
||||
return {
|
||||
showTooltip: false,
|
||||
filterWord: '',
|
||||
popperOptions: {
|
||||
modifiers: {
|
||||
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
|
||||
}
|
||||
}
|
||||
filterWord: ''
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Popover
|
||||
},
|
||||
methods: {
|
||||
openReactionSelect () {
|
||||
this.showTooltip = true
|
||||
this.filterWord = ''
|
||||
},
|
||||
closeReactionSelect () {
|
||||
this.showTooltip = false
|
||||
},
|
||||
addReaction (event, emoji) {
|
||||
addReaction (event, emoji, close) {
|
||||
const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
|
||||
if (existingReaction && existingReaction.me) {
|
||||
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
||||
} else {
|
||||
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
||||
}
|
||||
this.closeReactionSelect()
|
||||
close()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
commonEmojis () {
|
||||
return ['❤️', '😠', '👀', '😂', '🔥']
|
||||
return ['👍', '😠', '👀', '😂', '🔥']
|
||||
},
|
||||
emojis () {
|
||||
if (this.filterWord !== '') {
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
<template>
|
||||
<v-popover
|
||||
:popper-options="popperOptions"
|
||||
:open="showTooltip"
|
||||
trigger="manual"
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="top"
|
||||
:offset="{ y: 5 }"
|
||||
class="react-button-popover"
|
||||
@hide="closeReactionSelect"
|
||||
>
|
||||
<div slot="popover">
|
||||
<div
|
||||
slot="content"
|
||||
slot-scope="{close}"
|
||||
>
|
||||
<div class="reaction-picker-filter">
|
||||
<input
|
||||
v-model="filterWord"
|
||||
|
@ -19,7 +20,7 @@
|
|||
v-for="emoji in commonEmojis"
|
||||
:key="emoji"
|
||||
class="emoji-button"
|
||||
@click="addReaction($event, emoji)"
|
||||
@click="addReaction($event, emoji, close)"
|
||||
>
|
||||
{{ emoji }}
|
||||
</span>
|
||||
|
@ -28,23 +29,19 @@
|
|||
v-for="(emoji, key) in emojis"
|
||||
:key="key"
|
||||
class="emoji-button"
|
||||
@click="addReaction($event, emoji.replacement)"
|
||||
@click="addReaction($event, emoji.replacement, close)"
|
||||
>
|
||||
{{ emoji.replacement }}
|
||||
</span>
|
||||
<div class="reaction-bottom-fader" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="loggedIn"
|
||||
@click.prevent="openReactionSelect"
|
||||
>
|
||||
<i
|
||||
slot="trigger"
|
||||
class="icon-smile button-icon add-reaction-button"
|
||||
:title="$t('tool_tip.add_reaction')"
|
||||
/>
|
||||
</div>
|
||||
</v-popover>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script src="./react_button.js" ></script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { validationMixin } from 'vuelidate'
|
||||
import { required, sameAs } from 'vuelidate/lib/validators'
|
||||
import { required, requiredIf, sameAs } from 'vuelidate/lib/validators'
|
||||
import { mapActions, mapState } from 'vuex'
|
||||
|
||||
const registration = {
|
||||
|
@ -14,9 +14,10 @@ const registration = {
|
|||
},
|
||||
captcha: {}
|
||||
}),
|
||||
validations: {
|
||||
validations () {
|
||||
return {
|
||||
user: {
|
||||
email: { required },
|
||||
email: { required: requiredIf(() => this.accountActivationRequired) },
|
||||
username: { required },
|
||||
fullname: { required },
|
||||
password: { required },
|
||||
|
@ -25,6 +26,7 @@ const registration = {
|
|||
sameAsPassword: sameAs('password')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if ((!this.registrationOpen && !this.token) || this.signedIn) {
|
||||
|
@ -43,7 +45,8 @@ const registration = {
|
|||
signedIn: (state) => !!state.users.currentUser,
|
||||
isPending: (state) => state.users.signUpPending,
|
||||
serverValidationErrors: (state) => state.users.signUpErrors,
|
||||
termsOfService: (state) => state.instance.tos
|
||||
termsOfService: (state) => state.instance.tos,
|
||||
accountActivationRequired: (state) => state.instance.accountActivationRequired
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -187,6 +187,9 @@
|
|||
class="form-control"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -68,7 +68,12 @@
|
|||
|
||||
&-item-selected-inner {
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||
color: var(--selectedMenuText, $fallback--text);
|
||||
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuIcon, $fallback--icon);
|
||||
}
|
||||
|
||||
&-header {
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
/* eslint-env browser */
|
||||
import { filter, trim } from 'lodash'
|
||||
|
||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
||||
import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
|
||||
import { extractCommit } from '../../services/version/version.service'
|
||||
import { instanceDefaultProperties, defaultState as configDefaultState } from '../../modules/config.js'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
|
||||
const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
|
||||
const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/'
|
||||
|
||||
const multiChoiceProperties = [
|
||||
'postContentType',
|
||||
'subjectLineBehavior'
|
||||
]
|
||||
|
||||
const settings = {
|
||||
data () {
|
||||
const instance = this.$store.state.instance
|
||||
|
||||
return {
|
||||
loopSilentAvailable:
|
||||
// Firefox
|
||||
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
||||
// Chrome-likes
|
||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
|
||||
// Future spec, still not supported in Nightly 63 as of 08/2018
|
||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
|
||||
|
||||
backendVersion: instance.backendVersion,
|
||||
frontendVersion: instance.frontendVersion
|
||||
}
|
||||
},
|
||||
components: {
|
||||
TabSwitcher,
|
||||
StyleSwitcher,
|
||||
InterfaceLanguageSwitcher,
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
currentSaveStateNotice () {
|
||||
return this.$store.state.interface.settings.currentSaveStateNotice
|
||||
},
|
||||
postFormats () {
|
||||
return this.$store.state.instance.postFormats || []
|
||||
},
|
||||
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
|
||||
frontendVersionLink () {
|
||||
return pleromaFeCommitUrl + this.frontendVersion
|
||||
},
|
||||
backendVersionLink () {
|
||||
return pleromaBeCommitUrl + extractCommit(this.backendVersion)
|
||||
},
|
||||
// Getting localized values for instance-default properties
|
||||
...instanceDefaultProperties
|
||||
.filter(key => multiChoiceProperties.includes(key))
|
||||
.map(key => [
|
||||
key + 'DefaultValue',
|
||||
function () {
|
||||
return this.$store.getters.instanceDefaultConfig[key]
|
||||
}
|
||||
])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
...instanceDefaultProperties
|
||||
.filter(key => !multiChoiceProperties.includes(key))
|
||||
.map(key => [
|
||||
key + 'LocalizedValue',
|
||||
function () {
|
||||
return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key])
|
||||
}
|
||||
])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
// Generating computed values for vuex properties
|
||||
...Object.keys(configDefaultState)
|
||||
.map(key => [key, {
|
||||
get () { return this.$store.getters.mergedConfig[key] },
|
||||
set (value) {
|
||||
this.$store.dispatch('setOption', { name: key, value })
|
||||
}
|
||||
}])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
// Special cases (need to transform values or perform actions first)
|
||||
muteWordsString: {
|
||||
get () { return this.$store.getters.mergedConfig.muteWords.join('\n') },
|
||||
set (value) {
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'muteWords',
|
||||
value: filter(value.split('\n'), (word) => trim(word).length > 0)
|
||||
})
|
||||
}
|
||||
},
|
||||
useStreamingApi: {
|
||||
get () { return this.$store.getters.mergedConfig.useStreamingApi },
|
||||
set (value) {
|
||||
const promise = value
|
||||
? this.$store.dispatch('enableMastoSockets')
|
||||
: this.$store.dispatch('disableMastoSockets')
|
||||
|
||||
promise.then(() => {
|
||||
this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
|
||||
}).catch((e) => {
|
||||
console.error('Failed starting MastoAPI Streaming socket', e)
|
||||
this.$store.dispatch('disableMastoSockets')
|
||||
this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
// Updating nested properties
|
||||
watch: {
|
||||
notificationVisibility: {
|
||||
handler (value) {
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'notificationVisibility',
|
||||
value: this.$store.getters.mergedConfig.notificationVisibility
|
||||
})
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default settings
|
|
@ -1,424 +0,0 @@
|
|||
<template>
|
||||
<div class="settings panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="title">
|
||||
{{ $t('settings.settings') }}
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<template v-if="currentSaveStateNotice">
|
||||
<div
|
||||
v-if="currentSaveStateNotice.error"
|
||||
class="alert error"
|
||||
@click.prevent
|
||||
>
|
||||
{{ $t('settings.saving_err') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!currentSaveStateNotice.error"
|
||||
class="alert transparent"
|
||||
@click.prevent
|
||||
>
|
||||
{{ $t('settings.saving_ok') }}
|
||||
</div>
|
||||
</template>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<keep-alive>
|
||||
<tab-switcher>
|
||||
<div :label="$t('settings.general')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.interface') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<interface-language-switcher />
|
||||
</li>
|
||||
<li v-if="instanceSpecificPanelPresent">
|
||||
<Checkbox v-model="hideISP">
|
||||
{{ $t('settings.hide_isp') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('nav.timeline') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="hideMutedPosts">
|
||||
{{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="collapseMessageWithSubject">
|
||||
{{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="streaming">
|
||||
{{ $t('settings.streaming') }}
|
||||
</Checkbox>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
:class="[{disabled: !streaming}]"
|
||||
>
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="pauseOnUnfocused"
|
||||
:disabled="!streaming"
|
||||
>
|
||||
{{ $t('settings.pause_on_unfocused') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="useStreamingApi">
|
||||
{{ $t('settings.useStreamingApi') }}
|
||||
<br/>
|
||||
<small>
|
||||
{{ $t('settings.useStreamingApiWarning') }}
|
||||
</small>
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="autoLoad">
|
||||
{{ $t('settings.autoload') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="hoverPreview">
|
||||
{{ $t('settings.reply_link_preview') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="emojiReactionsOnTimeline">
|
||||
{{ $t('settings.emoji_reactions_on_timeline') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.composing') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="scopeCopy">
|
||||
{{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="alwaysShowSubjectInput">
|
||||
{{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
{{ $t('settings.subject_line_behavior') }}
|
||||
<label
|
||||
for="subjectLineBehavior"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="subjectLineBehavior"
|
||||
v-model="subjectLineBehavior"
|
||||
>
|
||||
<option value="email">
|
||||
{{ $t('settings.subject_line_email') }}
|
||||
{{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
<option value="masto">
|
||||
{{ $t('settings.subject_line_mastodon') }}
|
||||
{{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
<option value="noop">
|
||||
{{ $t('settings.subject_line_noop') }}
|
||||
{{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open" />
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="postFormats.length > 0">
|
||||
<div>
|
||||
{{ $t('settings.post_status_content_type') }}
|
||||
<label
|
||||
for="postContentType"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="postContentType"
|
||||
v-model="postContentType"
|
||||
>
|
||||
<option
|
||||
v-for="postFormat in postFormats"
|
||||
:key="postFormat"
|
||||
:value="postFormat"
|
||||
>
|
||||
{{ $t(`post_status.content_type["${postFormat}"]`) }}
|
||||
{{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open" />
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="minimalScopesMode">
|
||||
{{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="autohideFloatingPostButton">
|
||||
{{ $t('settings.autohide_floating_post_button') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="padEmoji">
|
||||
{{ $t('settings.pad_emoji') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.attachments') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="hideAttachments">
|
||||
{{ $t('settings.hide_attachments_in_tl') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="hideAttachmentsInConv">
|
||||
{{ $t('settings.hide_attachments_in_convo') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<label for="maxThumbnails">
|
||||
{{ $t('settings.max_thumbnails') }}
|
||||
</label>
|
||||
<input
|
||||
id="maxThumbnails"
|
||||
v-model.number="maxThumbnails"
|
||||
class="number-input"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="hideNsfw">
|
||||
{{ $t('settings.nsfw_clickthrough') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="preloadImage"
|
||||
:disabled="!hideNsfw"
|
||||
>
|
||||
{{ $t('settings.preload_images') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="useOneClickNsfw"
|
||||
:disabled="!hideNsfw"
|
||||
>
|
||||
{{ $t('settings.use_one_click_nsfw') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
<li>
|
||||
<Checkbox v-model="stopGifs">
|
||||
{{ $t('settings.stop_gifs') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="loopVideo">
|
||||
{{ $t('settings.loop_video') }}
|
||||
</Checkbox>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
:class="[{disabled: !streaming}]"
|
||||
>
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="loopVideoSilentOnly"
|
||||
:disabled="!loopVideo || !loopSilentAvailable"
|
||||
>
|
||||
{{ $t('settings.loop_video_silent_only') }}
|
||||
</Checkbox>
|
||||
<div
|
||||
v-if="!loopSilentAvailable"
|
||||
class="unavailable"
|
||||
>
|
||||
<i class="icon-globe" />! {{ $t('settings.limited_availability') }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="playVideosInModal">
|
||||
{{ $t('settings.play_videos_in_modal') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="useContainFit">
|
||||
{{ $t('settings.use_contain_fit') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.notifications') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="webPushNotifications">
|
||||
{{ $t('settings.enable_web_push_notifications') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.fun') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="greentext">
|
||||
{{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.theme')">
|
||||
<div class="setting-item">
|
||||
<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>
|
||||
<Checkbox v-model="notificationVisibility.likes">
|
||||
{{ $t('settings.notification_visibility_likes') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.repeats">
|
||||
{{ $t('settings.notification_visibility_repeats') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.follows">
|
||||
{{ $t('settings.notification_visibility_follows') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.mentions">
|
||||
{{ $t('settings.notification_visibility_mentions') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.moves">
|
||||
{{ $t('settings.notification_visibility_moves') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.emojiReactions">
|
||||
{{ $t('settings.notification_visibility_emoji_reactions') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
{{ $t('settings.replies_in_timeline') }}
|
||||
<label
|
||||
for="replyVisibility"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="replyVisibility"
|
||||
v-model="replyVisibility"
|
||||
>
|
||||
<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>
|
||||
<Checkbox v-model="hidePostStats">
|
||||
{{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox v-model="hideUserStats">
|
||||
{{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<p>{{ $t('settings.filtering_explanation') }}</p>
|
||||
<textarea
|
||||
id="muteWords"
|
||||
v-model="muteWordsString"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox v-model="hideFilteredStatuses">
|
||||
{{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :label="$t('settings.version.title')">
|
||||
<div class="setting-item">
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<p>{{ $t('settings.version.backend_version') }}</p>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<a
|
||||
:href="backendVersionLink"
|
||||
target="_blank"
|
||||
>{{ backendVersion }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>{{ $t('settings.version.frontend_version') }}</p>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<a
|
||||
:href="frontendVersionLink"
|
||||
target="_blank"
|
||||
>{{ frontendVersion }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./settings.js">
|
||||
</script>
|
|
@ -0,0 +1,58 @@
|
|||
import {
|
||||
instanceDefaultProperties,
|
||||
multiChoiceProperties,
|
||||
defaultState as configDefaultState
|
||||
} from 'src/modules/config.js'
|
||||
|
||||
const SharedComputedObject = () => ({
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
// Getting localized values for instance-default properties
|
||||
...instanceDefaultProperties
|
||||
.filter(key => multiChoiceProperties.includes(key))
|
||||
.map(key => [
|
||||
key + 'DefaultValue',
|
||||
function () {
|
||||
return this.$store.getters.instanceDefaultConfig[key]
|
||||
}
|
||||
])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
...instanceDefaultProperties
|
||||
.filter(key => !multiChoiceProperties.includes(key))
|
||||
.map(key => [
|
||||
key + 'LocalizedValue',
|
||||
function () {
|
||||
return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key])
|
||||
}
|
||||
])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
// Generating computed values for vuex properties
|
||||
...Object.keys(configDefaultState)
|
||||
.map(key => [key, {
|
||||
get () { return this.$store.getters.mergedConfig[key] },
|
||||
set (value) {
|
||||
this.$store.dispatch('setOption', { name: key, value })
|
||||
}
|
||||
}])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
// Special cases (need to transform values or perform actions first)
|
||||
useStreamingApi: {
|
||||
get () { return this.$store.getters.mergedConfig.useStreamingApi },
|
||||
set (value) {
|
||||
const promise = value
|
||||
? this.$store.dispatch('enableMastoSockets')
|
||||
: this.$store.dispatch('disableMastoSockets')
|
||||
|
||||
promise.then(() => {
|
||||
this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
|
||||
}).catch((e) => {
|
||||
console.error('Failed starting MastoAPI Streaming socket', e)
|
||||
this.$store.dispatch('disableMastoSockets')
|
||||
this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default SharedComputedObject
|
42
src/components/settings_modal/settings_modal.js
Normal file
42
src/components/settings_modal/settings_modal.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import Modal from 'src/components/modal/modal.vue'
|
||||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||
import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
|
||||
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
|
||||
|
||||
const SettingsModal = {
|
||||
components: {
|
||||
Modal,
|
||||
SettingsModalContent: getResettableAsyncComponent(
|
||||
() => import('./settings_modal_content.vue'),
|
||||
{
|
||||
loading: PanelLoading,
|
||||
error: AsyncComponentError,
|
||||
delay: 0
|
||||
}
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
closeModal () {
|
||||
this.$store.dispatch('closeSettingsModal')
|
||||
},
|
||||
peekModal () {
|
||||
this.$store.dispatch('togglePeekSettingsModal')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentSaveStateNotice () {
|
||||
return this.$store.state.interface.settings.currentSaveStateNotice
|
||||
},
|
||||
modalActivated () {
|
||||
return this.$store.state.interface.settingsModalState !== 'hidden'
|
||||
},
|
||||
modalOpenedOnce () {
|
||||
return this.$store.state.interface.settingsModalLoaded
|
||||
},
|
||||
modalPeeked () {
|
||||
return this.$store.state.interface.settingsModalState === 'minimized'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsModal
|
44
src/components/settings_modal/settings_modal.scss
Normal file
44
src/components/settings_modal/settings_modal.scss
Normal file
|
@ -0,0 +1,44 @@
|
|||
@import 'src/_variables.scss';
|
||||
.settings-modal {
|
||||
overflow: hidden;
|
||||
|
||||
&.peek {
|
||||
.settings-modal-panel {
|
||||
/* Explanation:
|
||||
* Modal is positioned vertically centered.
|
||||
* 100vh - 100% = Distance between modal's top+bottom boundaries and screen
|
||||
* (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen
|
||||
* + 100% - we move modal completely off-screen, it's top boundary touches
|
||||
* bottom of the screen
|
||||
* - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible
|
||||
*/
|
||||
transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));
|
||||
}
|
||||
}
|
||||
|
||||
.settings-modal-panel {
|
||||
overflow: hidden;
|
||||
transition: transform;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: 300ms;
|
||||
width: 1000px;
|
||||
max-width: 90vw;
|
||||
height: 90vh;
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
max-width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
|
||||
.btn {
|
||||
min-height: 28px;
|
||||
min-width: 10em;
|
||||
padding: 0 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
src/components/settings_modal/settings_modal.vue
Normal file
54
src/components/settings_modal/settings_modal.vue
Normal file
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<Modal
|
||||
:is-open="modalActivated"
|
||||
class="settings-modal"
|
||||
:class="{ peek: modalPeeked }"
|
||||
:no-background="modalPeeked"
|
||||
>
|
||||
<div class="settings-modal-panel panel">
|
||||
<div class="panel-heading">
|
||||
<span class="title">
|
||||
{{ $t('settings.settings') }}
|
||||
</span>
|
||||
<transition name="fade">
|
||||
<template v-if="currentSaveStateNotice">
|
||||
<div
|
||||
v-if="currentSaveStateNotice.error"
|
||||
class="alert error"
|
||||
@click.prevent
|
||||
>
|
||||
{{ $t('settings.saving_err') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!currentSaveStateNotice.error"
|
||||
class="alert transparent"
|
||||
@click.prevent
|
||||
>
|
||||
{{ $t('settings.saving_ok') }}
|
||||
</div>
|
||||
</template>
|
||||
</transition>
|
||||
<button
|
||||
class="btn"
|
||||
@click="peekModal"
|
||||
>
|
||||
{{ $t('general.peek') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('general.close') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<SettingsModalContent v-if="modalOpenedOnce" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script src="./settings_modal.js"></script>
|
||||
|
||||
<style src="./settings_modal.scss" lang="scss"></style>
|
34
src/components/settings_modal/settings_modal_content.js
Normal file
34
src/components/settings_modal/settings_modal_content.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
||||
|
||||
import DataImportExportTab from './tabs/data_import_export_tab.vue'
|
||||
import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'
|
||||
import NotificationsTab from './tabs/notifications_tab.vue'
|
||||
import FilteringTab from './tabs/filtering_tab.vue'
|
||||
import SecurityTab from './tabs/security_tab/security_tab.vue'
|
||||
import ProfileTab from './tabs/profile_tab.vue'
|
||||
import GeneralTab from './tabs/general_tab.vue'
|
||||
import VersionTab from './tabs/version_tab.vue'
|
||||
import ThemeTab from './tabs/theme_tab/theme_tab.vue'
|
||||
|
||||
const SettingsModalContent = {
|
||||
components: {
|
||||
TabSwitcher,
|
||||
|
||||
DataImportExportTab,
|
||||
MutesAndBlocksTab,
|
||||
NotificationsTab,
|
||||
FilteringTab,
|
||||
SecurityTab,
|
||||
ProfileTab,
|
||||
GeneralTab,
|
||||
VersionTab,
|
||||
ThemeTab
|
||||
},
|
||||
computed: {
|
||||
isLoggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsModalContent
|
43
src/components/settings_modal/settings_modal_content.scss
Normal file
43
src/components/settings_modal/settings_modal_content.scss
Normal file
|
@ -0,0 +1,43 @@
|
|||
@import 'src/_variables.scss';
|
||||
.settings_tab-switcher {
|
||||
height: 100%;
|
||||
|
||||
.setting-item {
|
||||
border-bottom: 2px solid var(--fg, $fallback--fg);
|
||||
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%;
|
||||
max-width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.unavailable,
|
||||
.unavailable i {
|
||||
color: var(--cRed, $fallback--cRed);
|
||||
color: $fallback--cRed;
|
||||
}
|
||||
|
||||
.number-input {
|
||||
max-width: 6em;
|
||||
}
|
||||
}
|
||||
}
|
73
src/components/settings_modal/settings_modal_content.vue
Normal file
73
src/components/settings_modal/settings_modal_content.vue
Normal file
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<tab-switcher
|
||||
ref="tabSwitcher"
|
||||
class="settings_tab-switcher"
|
||||
:side-tab-bar="true"
|
||||
:scrollable-tabs="true"
|
||||
>
|
||||
<div
|
||||
:label="$t('settings.general')"
|
||||
icon="wrench"
|
||||
>
|
||||
<GeneralTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.profile_tab')"
|
||||
icon="user"
|
||||
>
|
||||
<ProfileTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.security_tab')"
|
||||
icon="lock"
|
||||
>
|
||||
<SecurityTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('settings.filtering')"
|
||||
icon="filter"
|
||||
>
|
||||
<FilteringTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('settings.theme')"
|
||||
icon="brush"
|
||||
>
|
||||
<ThemeTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.notifications')"
|
||||
icon="bell-ringing-o"
|
||||
>
|
||||
<NotificationsTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.data_import_export_tab')"
|
||||
icon="download"
|
||||
>
|
||||
<DataImportExportTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.mutes_and_blocks')"
|
||||
:fullHeight="true"
|
||||
icon="eye-off"
|
||||
>
|
||||
<MutesAndBlocksTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('settings.version.title')"
|
||||
icon="info-circled"
|
||||
>
|
||||
<VersionTab />
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</template>
|
||||
|
||||
<script src="./settings_modal_content.js"></script>
|
||||
|
||||
<style src="./settings_modal_content.scss" lang="scss"></style>
|
65
src/components/settings_modal/tabs/data_import_export_tab.js
Normal file
65
src/components/settings_modal/tabs/data_import_export_tab.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
import Importer from 'src/components/importer/importer.vue'
|
||||
import Exporter from 'src/components/exporter/exporter.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const DataImportExportTab = {
|
||||
data () {
|
||||
return {
|
||||
activeTab: 'profile',
|
||||
newDomainToMute: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchTokens')
|
||||
},
|
||||
components: {
|
||||
Importer,
|
||||
Exporter,
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getFollowsContent () {
|
||||
return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
|
||||
.then(this.generateExportableUsersContent)
|
||||
},
|
||||
getBlocksContent () {
|
||||
return this.$store.state.api.backendInteractor.fetchBlocks()
|
||||
.then(this.generateExportableUsersContent)
|
||||
},
|
||||
importFollows (file) {
|
||||
return this.$store.state.api.backendInteractor.importFollows({ file })
|
||||
.then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
importBlocks (file) {
|
||||
return this.$store.state.api.backendInteractor.importBlocks({ file })
|
||||
.then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
generateExportableUsersContent (users) {
|
||||
// Get addresses
|
||||
return users.map((user) => {
|
||||
// check is it's a local user
|
||||
if (user && user.is_local) {
|
||||
// append the instance address
|
||||
// eslint-disable-next-line no-undef
|
||||
return user.screen_name + '@' + location.hostname
|
||||
}
|
||||
return user.screen_name
|
||||
}).join('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DataImportExportTab
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div
|
||||
:label="$t('settings.data_import_export_tab')"
|
||||
>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.follow_import') }}</h2>
|
||||
<p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
|
||||
<Importer
|
||||
:submit-handler="importFollows"
|
||||
:success-message="$t('settings.follows_imported')"
|
||||
:error-message="$t('settings.follow_import_error')"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.follow_export') }}</h2>
|
||||
<Exporter
|
||||
:get-content="getFollowsContent"
|
||||
filename="friends.csv"
|
||||
:export-button-label="$t('settings.follow_export_button')"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.block_import') }}</h2>
|
||||
<p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
|
||||
<Importer
|
||||
:submit-handler="importBlocks"
|
||||
:success-message="$t('settings.blocks_imported')"
|
||||
:error-message="$t('settings.block_import_error')"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.block_export') }}</h2>
|
||||
<Exporter
|
||||
:get-content="getBlocksContent"
|
||||
filename="blocks.csv"
|
||||
:export-button-label="$t('settings.block_export_button')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./data_import_export_tab.js"></script>
|
||||
<!-- <style lang="scss" src="./profile.scss"></style> -->
|
44
src/components/settings_modal/tabs/filtering_tab.js
Normal file
44
src/components/settings_modal/tabs/filtering_tab.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { filter, trim } from 'lodash'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
|
||||
const FilteringTab = {
|
||||
data () {
|
||||
return {
|
||||
muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n')
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject(),
|
||||
muteWordsString: {
|
||||
get () {
|
||||
return this.muteWordsStringLocal
|
||||
},
|
||||
set (value) {
|
||||
this.muteWordsStringLocal = value
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'muteWords',
|
||||
value: filter(value.split('\n'), (word) => trim(word).length > 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
// Updating nested properties
|
||||
watch: {
|
||||
notificationVisibility: {
|
||||
handler (value) {
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'notificationVisibility',
|
||||
value: this.$store.getters.mergedConfig.notificationVisibility
|
||||
})
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FilteringTab
|
86
src/components/settings_modal/tabs/filtering_tab.vue
Normal file
86
src/components/settings_modal/tabs/filtering_tab.vue
Normal file
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<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>
|
||||
<Checkbox v-model="notificationVisibility.likes">
|
||||
{{ $t('settings.notification_visibility_likes') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.repeats">
|
||||
{{ $t('settings.notification_visibility_repeats') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.follows">
|
||||
{{ $t('settings.notification_visibility_follows') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.mentions">
|
||||
{{ $t('settings.notification_visibility_mentions') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.moves">
|
||||
{{ $t('settings.notification_visibility_moves') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.emojiReactions">
|
||||
{{ $t('settings.notification_visibility_emoji_reactions') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
{{ $t('settings.replies_in_timeline') }}
|
||||
<label
|
||||
for="replyVisibility"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="replyVisibility"
|
||||
v-model="replyVisibility"
|
||||
>
|
||||
<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>
|
||||
<Checkbox v-model="hidePostStats">
|
||||
{{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox v-model="hideUserStats">
|
||||
{{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<p>{{ $t('settings.filtering_explanation') }}</p>
|
||||
<textarea
|
||||
id="muteWords"
|
||||
v-model="muteWordsString"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox v-model="hideFilteredStatuses">
|
||||
{{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./filtering_tab.js"></script>
|
31
src/components/settings_modal/tabs/general_tab.js
Normal file
31
src/components/settings_modal/tabs/general_tab.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
|
||||
const GeneralTab = {
|
||||
data () {
|
||||
return {
|
||||
loopSilentAvailable:
|
||||
// Firefox
|
||||
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
||||
// Chrome-likes
|
||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
|
||||
// Future spec, still not supported in Nightly 63 as of 08/2018
|
||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks')
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Checkbox,
|
||||
InterfaceLanguageSwitcher
|
||||
},
|
||||
computed: {
|
||||
postFormats () {
|
||||
return this.$store.state.instance.postFormats || []
|
||||
},
|
||||
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
|
||||
...SharedComputedObject()
|
||||
}
|
||||
}
|
||||
|
||||
export default GeneralTab
|
272
src/components/settings_modal/tabs/general_tab.vue
Normal file
272
src/components/settings_modal/tabs/general_tab.vue
Normal file
|
@ -0,0 +1,272 @@
|
|||
<template>
|
||||
<div :label="$t('settings.general')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.interface') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<interface-language-switcher />
|
||||
</li>
|
||||
<li v-if="instanceSpecificPanelPresent">
|
||||
<Checkbox v-model="hideISP">
|
||||
{{ $t('settings.hide_isp') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('nav.timeline') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="hideMutedPosts">
|
||||
{{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="collapseMessageWithSubject">
|
||||
{{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="streaming">
|
||||
{{ $t('settings.streaming') }}
|
||||
</Checkbox>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
:class="[{disabled: !streaming}]"
|
||||
>
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="pauseOnUnfocused"
|
||||
:disabled="!streaming"
|
||||
>
|
||||
{{ $t('settings.pause_on_unfocused') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="useStreamingApi">
|
||||
{{ $t('settings.useStreamingApi') }}
|
||||
<br>
|
||||
<small>
|
||||
{{ $t('settings.useStreamingApiWarning') }}
|
||||
</small>
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="autoLoad">
|
||||
{{ $t('settings.autoload') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="hoverPreview">
|
||||
{{ $t('settings.reply_link_preview') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="emojiReactionsOnTimeline">
|
||||
{{ $t('settings.emoji_reactions_on_timeline') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.composing') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="scopeCopy">
|
||||
{{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="alwaysShowSubjectInput">
|
||||
{{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
{{ $t('settings.subject_line_behavior') }}
|
||||
<label
|
||||
for="subjectLineBehavior"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="subjectLineBehavior"
|
||||
v-model="subjectLineBehavior"
|
||||
>
|
||||
<option value="email">
|
||||
{{ $t('settings.subject_line_email') }}
|
||||
{{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
<option value="masto">
|
||||
{{ $t('settings.subject_line_mastodon') }}
|
||||
{{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
<option value="noop">
|
||||
{{ $t('settings.subject_line_noop') }}
|
||||
{{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open" />
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="postFormats.length > 0">
|
||||
<div>
|
||||
{{ $t('settings.post_status_content_type') }}
|
||||
<label
|
||||
for="postContentType"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="postContentType"
|
||||
v-model="postContentType"
|
||||
>
|
||||
<option
|
||||
v-for="postFormat in postFormats"
|
||||
:key="postFormat"
|
||||
:value="postFormat"
|
||||
>
|
||||
{{ $t(`post_status.content_type["${postFormat}"]`) }}
|
||||
{{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open" />
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="minimalScopesMode">
|
||||
{{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="autohideFloatingPostButton">
|
||||
{{ $t('settings.autohide_floating_post_button') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="padEmoji">
|
||||
{{ $t('settings.pad_emoji') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.attachments') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="hideAttachments">
|
||||
{{ $t('settings.hide_attachments_in_tl') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="hideAttachmentsInConv">
|
||||
{{ $t('settings.hide_attachments_in_convo') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<label for="maxThumbnails">
|
||||
{{ $t('settings.max_thumbnails') }}
|
||||
</label>
|
||||
<input
|
||||
id="maxThumbnails"
|
||||
v-model.number="maxThumbnails"
|
||||
class="number-input"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="hideNsfw">
|
||||
{{ $t('settings.nsfw_clickthrough') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="preloadImage"
|
||||
:disabled="!hideNsfw"
|
||||
>
|
||||
{{ $t('settings.preload_images') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="useOneClickNsfw"
|
||||
:disabled="!hideNsfw"
|
||||
>
|
||||
{{ $t('settings.use_one_click_nsfw') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
<li>
|
||||
<Checkbox v-model="stopGifs">
|
||||
{{ $t('settings.stop_gifs') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="loopVideo">
|
||||
{{ $t('settings.loop_video') }}
|
||||
</Checkbox>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
:class="[{disabled: !streaming}]"
|
||||
>
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="loopVideoSilentOnly"
|
||||
:disabled="!loopVideo || !loopSilentAvailable"
|
||||
>
|
||||
{{ $t('settings.loop_video_silent_only') }}
|
||||
</Checkbox>
|
||||
<div
|
||||
v-if="!loopSilentAvailable"
|
||||
class="unavailable"
|
||||
>
|
||||
<i class="icon-globe" />! {{ $t('settings.limited_availability') }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="playVideosInModal">
|
||||
{{ $t('settings.play_videos_in_modal') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="useContainFit">
|
||||
{{ $t('settings.use_contain_fit') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.notifications') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="webPushNotifications">
|
||||
{{ $t('settings.enable_web_push_notifications') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.fun') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="greentext">
|
||||
{{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./general_tab.js"></script>
|
136
src/components/settings_modal/tabs/mutes_and_blocks_tab.js
Normal file
136
src/components/settings_modal/tabs/mutes_and_blocks_tab.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
import get from 'lodash/get'
|
||||
import map from 'lodash/map'
|
||||
import reject from 'lodash/reject'
|
||||
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
||||
import BlockCard from 'src/components/block_card/block_card.vue'
|
||||
import MuteCard from 'src/components/mute_card/mute_card.vue'
|
||||
import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue'
|
||||
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
|
||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const BlockList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const MuteList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchMutes'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const DomainMuteList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const MutesAndBlocks = {
|
||||
data () {
|
||||
return {
|
||||
activeTab: 'profile'
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchTokens')
|
||||
this.$store.dispatch('getKnownDomains')
|
||||
},
|
||||
components: {
|
||||
TabSwitcher,
|
||||
BlockList,
|
||||
MuteList,
|
||||
DomainMuteList,
|
||||
BlockCard,
|
||||
MuteCard,
|
||||
DomainMuteCard,
|
||||
ProgressButton,
|
||||
Autosuggest,
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
knownDomains () {
|
||||
return this.$store.state.instance.knownDomains
|
||||
},
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
importFollows (file) {
|
||||
return this.$store.state.api.backendInteractor.importFollows({ file })
|
||||
.then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
importBlocks (file) {
|
||||
return this.$store.state.api.backendInteractor.importBlocks({ file })
|
||||
.then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
generateExportableUsersContent (users) {
|
||||
// Get addresses
|
||||
return users.map((user) => {
|
||||
// check is it's a local user
|
||||
if (user && user.is_local) {
|
||||
// append the instance address
|
||||
// eslint-disable-next-line no-undef
|
||||
return user.screen_name + '@' + location.hostname
|
||||
}
|
||||
return user.screen_name
|
||||
}).join('\n')
|
||||
},
|
||||
activateTab (tabName) {
|
||||
this.activeTab = tabName
|
||||
},
|
||||
filterUnblockedUsers (userIds) {
|
||||
return reject(userIds, (userId) => {
|
||||
const relationship = this.$store.getters.relationship(this.userId)
|
||||
return relationship.blocking || userId === this.user.id
|
||||
})
|
||||
},
|
||||
filterUnMutedUsers (userIds) {
|
||||
return reject(userIds, (userId) => {
|
||||
const relationship = this.$store.getters.relationship(this.userId)
|
||||
return relationship.muting || userId === this.user.id
|
||||
})
|
||||
},
|
||||
queryUserIds (query) {
|
||||
return this.$store.dispatch('searchUsers', { query })
|
||||
.then((users) => map(users, 'id'))
|
||||
},
|
||||
blockUsers (ids) {
|
||||
return this.$store.dispatch('blockUsers', ids)
|
||||
},
|
||||
unblockUsers (ids) {
|
||||
return this.$store.dispatch('unblockUsers', ids)
|
||||
},
|
||||
muteUsers (ids) {
|
||||
return this.$store.dispatch('muteUsers', ids)
|
||||
},
|
||||
unmuteUsers (ids) {
|
||||
return this.$store.dispatch('unmuteUsers', ids)
|
||||
},
|
||||
filterUnMutedDomains (urls) {
|
||||
return urls.filter(url => !this.user.domainMutes.includes(url))
|
||||
},
|
||||
queryKnownDomains (query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(this.knownDomains.filter(url => url.toLowerCase().includes(query)))
|
||||
})
|
||||
},
|
||||
unmuteDomains (domains) {
|
||||
return this.$store.dispatch('unmuteDomains', domains)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MutesAndBlocks
|
29
src/components/settings_modal/tabs/mutes_and_blocks_tab.scss
Normal file
29
src/components/settings_modal/tabs/mutes_and_blocks_tab.scss
Normal file
|
@ -0,0 +1,29 @@
|
|||
.mutes-and-blocks-tab {
|
||||
height: 100%;
|
||||
|
||||
.usersearch-wrapper {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.bulk-actions {
|
||||
text-align: right;
|
||||
padding: 0 1em;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.bulk-action-button {
|
||||
width: 10em
|
||||
}
|
||||
|
||||
.domain-mute-form {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.domain-mute-button {
|
||||
align-self: flex-end;
|
||||
margin-top: 1em;
|
||||
width: 10em
|
||||
}
|
||||
}
|
171
src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
Normal file
171
src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
Normal file
|
@ -0,0 +1,171 @@
|
|||
<template>
|
||||
<tab-switcher
|
||||
:scrollable-tabs="true"
|
||||
class="mutes-and-blocks-tab"
|
||||
>
|
||||
<div :label="$t('settings.blocks_tab')">
|
||||
<div class="usersearch-wrapper">
|
||||
<Autosuggest
|
||||
:filter="filterUnblockedUsers"
|
||||
:query="queryUserIds"
|
||||
:placeholder="$t('settings.search_user_to_block')"
|
||||
>
|
||||
<BlockCard
|
||||
slot-scope="row"
|
||||
:user-id="row.item"
|
||||
/>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<BlockList
|
||||
:refresh="true"
|
||||
:get-key="i => i"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
slot-scope="{selected}"
|
||||
>
|
||||
<div class="bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default bulk-action-button"
|
||||
:click="() => blockUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.block') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.block_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => unblockUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.unblock') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.unblock_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
slot="item"
|
||||
slot-scope="{item}"
|
||||
>
|
||||
<BlockCard :user-id="item" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
{{ $t('settings.no_blocks') }}
|
||||
</template>
|
||||
</BlockList>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.mutes_tab')">
|
||||
<tab-switcher>
|
||||
<div label="Users">
|
||||
<div class="usersearch-wrapper">
|
||||
<Autosuggest
|
||||
:filter="filterUnMutedUsers"
|
||||
:query="queryUserIds"
|
||||
:placeholder="$t('settings.search_user_to_mute')"
|
||||
>
|
||||
<MuteCard
|
||||
slot-scope="row"
|
||||
:user-id="row.item"
|
||||
/>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<MuteList
|
||||
:refresh="true"
|
||||
:get-key="i => i"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
slot-scope="{selected}"
|
||||
>
|
||||
<div class="bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => muteUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.mute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => unmuteUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.unmute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
slot="item"
|
||||
slot-scope="{item}"
|
||||
>
|
||||
<MuteCard :user-id="item" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</MuteList>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.domain_mutes')">
|
||||
<div class="domain-mute-form">
|
||||
<Autosuggest
|
||||
:filter="filterUnMutedDomains"
|
||||
:query="queryKnownDomains"
|
||||
:placeholder="$t('settings.type_domains_to_mute')"
|
||||
>
|
||||
<DomainMuteCard
|
||||
slot-scope="row"
|
||||
:domain="row.item"
|
||||
/>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<DomainMuteList
|
||||
:refresh="true"
|
||||
:get-key="i => i"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
slot-scope="{selected}"
|
||||
>
|
||||
<div class="bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => unmuteDomains(selected)"
|
||||
>
|
||||
{{ $t('domain_mute_card.unmute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
slot="item"
|
||||
slot-scope="{item}"
|
||||
>
|
||||
<DomainMuteCard :domain="item" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</DomainMuteList>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</template>
|
||||
|
||||
<script src="./mutes_and_blocks_tab.js"></script>
|
||||
<style lang="scss" src="./mutes_and_blocks_tab.scss"></style>
|
27
src/components/settings_modal/tabs/notifications_tab.js
Normal file
27
src/components/settings_modal/tabs/notifications_tab.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const NotificationsTab = {
|
||||
data () {
|
||||
return {
|
||||
activeTab: 'profile',
|
||||
notificationSettings: this.$store.state.users.currentUser.notification_settings,
|
||||
newDomainToMute: ''
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateNotificationSettings () {
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateNotificationSettings({ settings: this.notificationSettings })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationsTab
|
54
src/components/settings_modal/tabs/notifications_tab.vue
Normal file
54
src/components/settings_modal/tabs/notifications_tab.vue
Normal file
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<div :label="$t('settings.notifications')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
|
||||
<div class="select-multiple">
|
||||
<span class="label">{{ $t('settings.notification_setting') }}</span>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.follows">
|
||||
{{ $t('settings.notification_setting_follows') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.followers">
|
||||
{{ $t('settings.notification_setting_followers') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.non_follows">
|
||||
{{ $t('settings.notification_setting_non_follows') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.non_followers">
|
||||
{{ $t('settings.notification_setting_non_followers') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.notification_setting_privacy') }}</h2>
|
||||
<p>
|
||||
<Checkbox v-model="notificationSettings.privacy_option">
|
||||
{{ $t('settings.notification_setting_privacy_option') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<p>{{ $t('settings.notification_mutes') }}</p>
|
||||
<p>{{ $t('settings.notification_blocks') }}</p>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="updateNotificationSettings"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./notifications_tab.js"></script>
|
||||
<!-- <style lang="scss" src="./profile.scss"></style> -->
|
179
src/components/settings_modal/tabs/profile_tab.js
Normal file
179
src/components/settings_modal/tabs/profile_tab.js
Normal file
|
@ -0,0 +1,179 @@
|
|||
import unescape from 'lodash/unescape'
|
||||
import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
|
||||
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
||||
import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
|
||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
|
||||
import suggestor from 'src/components/emoji_input/suggestor.js'
|
||||
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const ProfileTab = {
|
||||
data () {
|
||||
return {
|
||||
newName: this.$store.state.users.currentUser.name,
|
||||
newBio: unescape(this.$store.state.users.currentUser.description),
|
||||
newLocked: this.$store.state.users.currentUser.locked,
|
||||
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
|
||||
newDefaultScope: this.$store.state.users.currentUser.default_scope,
|
||||
hideFollows: this.$store.state.users.currentUser.hide_follows,
|
||||
hideFollowers: this.$store.state.users.currentUser.hide_followers,
|
||||
hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
|
||||
hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count,
|
||||
showRole: this.$store.state.users.currentUser.show_role,
|
||||
role: this.$store.state.users.currentUser.role,
|
||||
discoverable: this.$store.state.users.currentUser.discoverable,
|
||||
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
||||
pickAvatarBtnVisible: true,
|
||||
bannerUploading: false,
|
||||
backgroundUploading: false,
|
||||
banner: null,
|
||||
bannerPreview: null,
|
||||
background: null,
|
||||
backgroundPreview: null,
|
||||
bannerUploadError: null,
|
||||
backgroundUploadError: null
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ScopeSelector,
|
||||
ImageCropper,
|
||||
EmojiInput,
|
||||
Autosuggest,
|
||||
ProgressButton,
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
emojiUserSuggestor () {
|
||||
return suggestor({
|
||||
emoji: [
|
||||
...this.$store.state.instance.emoji,
|
||||
...this.$store.state.instance.customEmoji
|
||||
],
|
||||
users: this.$store.state.users.users,
|
||||
updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
|
||||
})
|
||||
},
|
||||
emojiSuggestor () {
|
||||
return suggestor({ emoji: [
|
||||
...this.$store.state.instance.emoji,
|
||||
...this.$store.state.instance.customEmoji
|
||||
] })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateProfile () {
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateProfile({
|
||||
params: {
|
||||
note: this.newBio,
|
||||
locked: this.newLocked,
|
||||
// Backend notation.
|
||||
/* eslint-disable camelcase */
|
||||
display_name: this.newName,
|
||||
default_scope: this.newDefaultScope,
|
||||
no_rich_text: this.newNoRichText,
|
||||
hide_follows: this.hideFollows,
|
||||
hide_followers: this.hideFollowers,
|
||||
discoverable: this.discoverable,
|
||||
allow_following_move: this.allowFollowingMove,
|
||||
hide_follows_count: this.hideFollowsCount,
|
||||
hide_followers_count: this.hideFollowersCount,
|
||||
show_role: this.showRole
|
||||
/* eslint-enable camelcase */
|
||||
} }).then((user) => {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
})
|
||||
},
|
||||
changeVis (visibility) {
|
||||
this.newDefaultScope = visibility
|
||||
},
|
||||
uploadFile (slot, e) {
|
||||
const file = e.target.files[0]
|
||||
if (!file) { return }
|
||||
if (file.size > this.$store.state.instance[slot + 'limit']) {
|
||||
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
|
||||
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
|
||||
this[slot + 'UploadError'] = [
|
||||
this.$t('upload.error.base'),
|
||||
this.$t(
|
||||
'upload.error.file_too_big',
|
||||
{
|
||||
filesize: filesize.num,
|
||||
filesizeunit: filesize.unit,
|
||||
allowedsize: allowedsize.num,
|
||||
allowedsizeunit: allowedsize.unit
|
||||
}
|
||||
)
|
||||
].join(' ')
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const reader = new FileReader()
|
||||
reader.onload = ({ target }) => {
|
||||
const img = target.result
|
||||
this[slot + 'Preview'] = img
|
||||
this[slot] = file
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
},
|
||||
submitAvatar (cropper, file) {
|
||||
const that = this
|
||||
return new Promise((resolve, reject) => {
|
||||
function updateAvatar (avatar) {
|
||||
that.$store.state.api.backendInteractor.updateAvatar({ avatar })
|
||||
.then((user) => {
|
||||
that.$store.commit('addNewUsers', [user])
|
||||
that.$store.commit('setCurrentUser', user)
|
||||
resolve()
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
|
||||
})
|
||||
}
|
||||
|
||||
if (cropper) {
|
||||
cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
|
||||
} else {
|
||||
updateAvatar(file)
|
||||
}
|
||||
})
|
||||
},
|
||||
submitBanner () {
|
||||
if (!this.bannerPreview) { return }
|
||||
|
||||
this.bannerUploading = true
|
||||
this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
|
||||
.then((user) => {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
this.bannerPreview = null
|
||||
})
|
||||
.catch((err) => {
|
||||
this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
|
||||
})
|
||||
.then(() => { this.bannerUploading = false })
|
||||
},
|
||||
submitBg () {
|
||||
if (!this.backgroundPreview) { return }
|
||||
let background = this.background
|
||||
this.backgroundUploading = true
|
||||
this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
|
||||
if (!data.error) {
|
||||
this.$store.commit('addNewUsers', [data])
|
||||
this.$store.commit('setCurrentUser', data)
|
||||
this.backgroundPreview = null
|
||||
} else {
|
||||
this.backgroundUploadError = this.$t('upload.error.base') + data.error
|
||||
}
|
||||
this.backgroundUploading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProfileTab
|
82
src/components/settings_modal/tabs/profile_tab.scss
Normal file
82
src/components/settings_modal/tabs/profile_tab.scss
Normal file
|
@ -0,0 +1,82 @@
|
|||
@import '../../../_variables.scss';
|
||||
.profile-tab {
|
||||
.bio {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.visibility-tray {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
input[type=file] {
|
||||
padding: 5px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.banner {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.uploading {
|
||||
font-size: 1.5em;
|
||||
margin: 0.25em;
|
||||
}
|
||||
|
||||
.name-changer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bg {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.current-avatar {
|
||||
display: block;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border-radius: $fallback--avatarRadius;
|
||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||
}
|
||||
|
||||
.oauth-tokens {
|
||||
width: 100%;
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&-usersearch-wrapper {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
&-bulk-actions {
|
||||
text-align: right;
|
||||
padding: 0 1em;
|
||||
min-height: 28px;
|
||||
|
||||
button {
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
&-domain-mute-form {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
button {
|
||||
align-self: flex-end;
|
||||
margin-top: 1em;
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-subitem {
|
||||
margin-left: 1.75em;
|
||||
}
|
||||
}
|
213
src/components/settings_modal/tabs/profile_tab.vue
Normal file
213
src/components/settings_modal/tabs/profile_tab.vue
Normal file
|
@ -0,0 +1,213 @@
|
|||
<template>
|
||||
<div class="profile-tab">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.name_bio') }}</h2>
|
||||
<p>{{ $t('settings.name') }}</p>
|
||||
<EmojiInput
|
||||
v-model="newName"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiSuggestor"
|
||||
>
|
||||
<input
|
||||
id="username"
|
||||
v-model="newName"
|
||||
classname="name-changer"
|
||||
>
|
||||
</EmojiInput>
|
||||
<p>{{ $t('settings.bio') }}</p>
|
||||
<EmojiInput
|
||||
v-model="newBio"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiUserSuggestor"
|
||||
>
|
||||
<textarea
|
||||
v-model="newBio"
|
||||
classname="bio"
|
||||
/>
|
||||
</EmojiInput>
|
||||
<p>
|
||||
<Checkbox v-model="newLocked">
|
||||
{{ $t('settings.lock_account_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<div>
|
||||
<label for="default-vis">{{ $t('settings.default_vis') }}</label>
|
||||
<div
|
||||
id="default-vis"
|
||||
class="visibility-tray"
|
||||
>
|
||||
<scope-selector
|
||||
:show-all="true"
|
||||
:user-default="newDefaultScope"
|
||||
:initial-scope="newDefaultScope"
|
||||
:on-scope-change="changeVis"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<Checkbox v-model="newNoRichText">
|
||||
{{ $t('settings.no_rich_text_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox v-model="hideFollows">
|
||||
{{ $t('settings.hide_follows_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p class="setting-subitem">
|
||||
<Checkbox
|
||||
v-model="hideFollowsCount"
|
||||
:disabled="!hideFollows"
|
||||
>
|
||||
{{ $t('settings.hide_follows_count_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox v-model="hideFollowers">
|
||||
{{ $t('settings.hide_followers_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p class="setting-subitem">
|
||||
<Checkbox
|
||||
v-model="hideFollowersCount"
|
||||
:disabled="!hideFollowers"
|
||||
>
|
||||
{{ $t('settings.hide_followers_count_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox v-model="allowFollowingMove">
|
||||
{{ $t('settings.allow_following_move') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p v-if="role === 'admin' || role === 'moderator'">
|
||||
<Checkbox v-model="showRole">
|
||||
<template v-if="role === 'admin'">
|
||||
{{ $t('settings.show_admin_badge') }}
|
||||
</template>
|
||||
<template v-if="role === 'moderator'">
|
||||
{{ $t('settings.show_moderator_badge') }}
|
||||
</template>
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox v-model="discoverable">
|
||||
{{ $t('settings.discoverable') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<button
|
||||
:disabled="newName && newName.length === 0"
|
||||
class="btn btn-default"
|
||||
@click="updateProfile"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.avatar') }}</h2>
|
||||
<p class="visibility-notice">
|
||||
{{ $t('settings.avatar_size_instruction') }}
|
||||
</p>
|
||||
<p>{{ $t('settings.current_avatar') }}</p>
|
||||
<img
|
||||
:src="user.profile_image_url_original"
|
||||
class="current-avatar"
|
||||
>
|
||||
<p>{{ $t('settings.set_new_avatar') }}</p>
|
||||
<button
|
||||
v-show="pickAvatarBtnVisible"
|
||||
id="pick-avatar"
|
||||
class="btn"
|
||||
type="button"
|
||||
>
|
||||
{{ $t('settings.upload_a_photo') }}
|
||||
</button>
|
||||
<image-cropper
|
||||
trigger="#pick-avatar"
|
||||
:submit-handler="submitAvatar"
|
||||
@open="pickAvatarBtnVisible=false"
|
||||
@close="pickAvatarBtnVisible=true"
|
||||
/>
|
||||
</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"
|
||||
>
|
||||
<p>{{ $t('settings.set_new_profile_banner') }}</p>
|
||||
<img
|
||||
v-if="bannerPreview"
|
||||
class="banner"
|
||||
:src="bannerPreview"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
@change="uploadFile('banner', $event)"
|
||||
>
|
||||
</div>
|
||||
<i
|
||||
v-if="bannerUploading"
|
||||
class=" icon-spin4 animate-spin uploading"
|
||||
/>
|
||||
<button
|
||||
v-else-if="bannerPreview"
|
||||
class="btn btn-default"
|
||||
@click="submitBanner"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="bannerUploadError"
|
||||
class="alert error"
|
||||
>
|
||||
Error: {{ bannerUploadError }}
|
||||
<i
|
||||
class="button-icon icon-cancel"
|
||||
@click="clearUploadError('banner')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.profile_background') }}</h2>
|
||||
<p>{{ $t('settings.set_new_profile_background') }}</p>
|
||||
<img
|
||||
v-if="backgroundPreview"
|
||||
class="bg"
|
||||
:src="backgroundPreview"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
@change="uploadFile('background', $event)"
|
||||
>
|
||||
</div>
|
||||
<i
|
||||
v-if="backgroundUploading"
|
||||
class=" icon-spin4 animate-spin uploading"
|
||||
/>
|
||||
<button
|
||||
v-else-if="backgroundPreview"
|
||||
class="btn btn-default"
|
||||
@click="submitBg"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="backgroundUploadError"
|
||||
class="alert error"
|
||||
>
|
||||
Error: {{ backgroundUploadError }}
|
||||
<i
|
||||
class="button-icon icon-cancel"
|
||||
@click="clearUploadError('background')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./profile_tab.js"></script>
|
||||
<style lang="scss" src="./profile_tab.scss"></style>
|
|
@ -137,20 +137,20 @@
|
|||
|
||||
<script src="./mfa.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
@import '../../../../_variables.scss';
|
||||
.mfa-settings {
|
||||
.mfa-heading, .method-item {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
|
||||
.setup-otp {
|
||||
display: flex;
|
||||
justify-content: center;
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="mfa-backup-codes">
|
||||
<h4 v-if="displayTitle">
|
||||
{{ $t('settings.mfa.recovery_codes') }}
|
||||
</h4>
|
||||
|
@ -21,13 +21,15 @@
|
|||
</template>
|
||||
<script src="./mfa_backup_codes.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../../../../_variables.scss';
|
||||
|
||||
.warning {
|
||||
.mfa-backup-codes {
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
.backup-codes {
|
||||
}
|
||||
.backup-codes {
|
||||
font-family: var(--postCodeFont, monospace);
|
||||
}
|
||||
}
|
||||
</style>
|
106
src/components/settings_modal/tabs/security_tab/security_tab.js
Normal file
106
src/components/settings_modal/tabs/security_tab/security_tab.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Mfa from './mfa.vue'
|
||||
|
||||
const SecurityTab = {
|
||||
data () {
|
||||
return {
|
||||
newEmail: '',
|
||||
changeEmailError: false,
|
||||
changeEmailPassword: '',
|
||||
changedEmail: false,
|
||||
deletingAccount: false,
|
||||
deleteAccountConfirmPasswordInput: '',
|
||||
deleteAccountError: false,
|
||||
changePasswordInputs: [ '', '', '' ],
|
||||
changedPassword: false,
|
||||
changePasswordError: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchTokens')
|
||||
},
|
||||
components: {
|
||||
ProgressButton,
|
||||
Mfa,
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
pleromaBackend () {
|
||||
return this.$store.state.instance.pleromaBackend
|
||||
},
|
||||
oauthTokens () {
|
||||
return this.$store.state.oauthTokens.tokens.map(oauthToken => {
|
||||
return {
|
||||
id: oauthToken.id,
|
||||
appName: oauthToken.app_name,
|
||||
validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
confirmDelete () {
|
||||
this.deletingAccount = true
|
||||
},
|
||||
deleteAccount () {
|
||||
this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
|
||||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.$store.dispatch('logout')
|
||||
this.$router.push({ name: 'root' })
|
||||
} else {
|
||||
this.deleteAccountError = res.error
|
||||
}
|
||||
})
|
||||
},
|
||||
changePassword () {
|
||||
const params = {
|
||||
password: this.changePasswordInputs[0],
|
||||
newPassword: this.changePasswordInputs[1],
|
||||
newPasswordConfirmation: this.changePasswordInputs[2]
|
||||
}
|
||||
this.$store.state.api.backendInteractor.changePassword(params)
|
||||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.changedPassword = true
|
||||
this.changePasswordError = false
|
||||
this.logout()
|
||||
} else {
|
||||
this.changedPassword = false
|
||||
this.changePasswordError = res.error
|
||||
}
|
||||
})
|
||||
},
|
||||
changeEmail () {
|
||||
const params = {
|
||||
email: this.newEmail,
|
||||
password: this.changeEmailPassword
|
||||
}
|
||||
this.$store.state.api.backendInteractor.changeEmail(params)
|
||||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.changedEmail = true
|
||||
this.changeEmailError = false
|
||||
} else {
|
||||
this.changedEmail = false
|
||||
this.changeEmailError = res.error
|
||||
}
|
||||
})
|
||||
},
|
||||
logout () {
|
||||
this.$store.dispatch('logout')
|
||||
this.$router.replace('/')
|
||||
},
|
||||
revokeToken (id) {
|
||||
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
|
||||
this.$store.dispatch('revokeToken', id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SecurityTab
|
143
src/components/settings_modal/tabs/security_tab/security_tab.vue
Normal file
143
src/components/settings_modal/tabs/security_tab/security_tab.vue
Normal file
|
@ -0,0 +1,143 @@
|
|||
<template>
|
||||
<div :label="$t('settings.security_tab')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.change_email') }}</h2>
|
||||
<div>
|
||||
<p>{{ $t('settings.new_email') }}</p>
|
||||
<input
|
||||
v-model="newEmail"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ $t('settings.current_password') }}</p>
|
||||
<input
|
||||
v-model="changeEmailPassword"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="changeEmail"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
<p v-if="changedEmail">
|
||||
{{ $t('settings.changed_email') }}
|
||||
</p>
|
||||
<template v-if="changeEmailError !== false">
|
||||
<p>{{ $t('settings.change_email_error') }}</p>
|
||||
<p>{{ changeEmailError }}</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.change_password') }}</h2>
|
||||
<div>
|
||||
<p>{{ $t('settings.current_password') }}</p>
|
||||
<input
|
||||
v-model="changePasswordInputs[0]"
|
||||
type="password"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ $t('settings.new_password') }}</p>
|
||||
<input
|
||||
v-model="changePasswordInputs[1]"
|
||||
type="password"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ $t('settings.confirm_new_password') }}</p>
|
||||
<input
|
||||
v-model="changePasswordInputs[2]"
|
||||
type="password"
|
||||
>
|
||||
</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.oauth_tokens') }}</h2>
|
||||
<table class="oauth-tokens">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('settings.app_name') }}</th>
|
||||
<th>{{ $t('settings.valid_until') }}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="oauthToken in oauthTokens"
|
||||
:key="oauthToken.id"
|
||||
>
|
||||
<td>{{ oauthToken.appName }}</td>
|
||||
<td>{{ oauthToken.validUntil }}</td>
|
||||
<td class="actions">
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="revokeToken(oauthToken.id)"
|
||||
>
|
||||
{{ $t('settings.revoke_token') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<mfa />
|
||||
<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
|
||||
v-model="deleteAccountConfirmPasswordInput"
|
||||
type="password"
|
||||
>
|
||||
<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
|
||||
v-if="!deletingAccount"
|
||||
class="btn btn-default"
|
||||
@click="confirmDelete"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./security_tab.js"></script>
|
||||
<!-- <style lang="scss" src="./profile.scss"></style> -->
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue