forked from AkkomaGang/akkoma-fe
Merge branch 'develop' of git.pleroma.social:pleroma/pleroma-fe into develop
This commit is contained in:
commit
12519a54b5
29 changed files with 538 additions and 109 deletions
|
@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
### Changed
|
### Changed
|
||||||
- Greentext now has separate color slot for it
|
- Greentext now has separate color slot for it
|
||||||
- Removed the use of with_move parameters when fetching notifications
|
- Removed the use of with_move parameters when fetching notifications
|
||||||
|
- Push notifications now are the same as normal notfication, and are localized.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Weird bug related to post being sent seemingly after pasting with keyboard (hopefully)
|
- Weird bug related to post being sent seemingly after pasting with keyboard (hopefully)
|
||||||
|
@ -16,6 +17,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Added private notifications option for push notifications
|
- Added private notifications option for push notifications
|
||||||
- 'Copy link' button for statuses (in the ellipsis menu)
|
- 'Copy link' button for statuses (in the ellipsis menu)
|
||||||
- Autocomplete domains from list of known instances
|
- Autocomplete domains from list of known instances
|
||||||
|
- 'Bot' settings option and badge
|
||||||
|
- Added profile meta data fields that can be set in profile settings
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Registration page no longer requires email if the server is configured not to require it
|
- Registration page no longer requires email if the server is configured not to require it
|
||||||
|
@ -25,12 +28,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Add better visual indication for drag-and-drop for files
|
- Add better visual indication for drag-and-drop for files
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- Custom Emoji will display in poll options now.
|
||||||
- Status ellipsis menu closes properly when selecting certain options
|
- Status ellipsis menu closes properly when selecting certain options
|
||||||
- Cropped images look correct in Chrome
|
- Cropped images look correct in Chrome
|
||||||
- Newlines in the muted words settings work again
|
- Newlines in the muted words settings work again
|
||||||
- Clicking on non-latin hashtags won't open a new window
|
- Clicking on non-latin hashtags won't open a new window
|
||||||
- Uploading and drag-dropping multiple files works correctly now.
|
- Uploading and drag-dropping multiple files works correctly now.
|
||||||
- Subject field now appears disabled when posting
|
- Subject field now appears disabled when posting
|
||||||
|
- Fix status ellipsis menu being cut off in notifications column
|
||||||
|
- Fixed autocomplete sometimes not returning the right user when there's already some results
|
||||||
|
|
||||||
## [2.0.3] - 2020-05-02
|
## [2.0.3] - 2020-05-02
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
@ -8,38 +8,64 @@ import backendInteractorService from '../services/backend_interactor_service/bac
|
||||||
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
||||||
import { applyTheme } from '../services/style_setter/style_setter.js'
|
import { applyTheme } from '../services/style_setter/style_setter.js'
|
||||||
|
|
||||||
const getStatusnetConfig = async ({ store }) => {
|
let staticInitialResults = null
|
||||||
|
|
||||||
|
const parsedInitialResults = () => {
|
||||||
|
if (!document.getElementById('initial-results')) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (!staticInitialResults) {
|
||||||
|
staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent)
|
||||||
|
}
|
||||||
|
return staticInitialResults
|
||||||
|
}
|
||||||
|
|
||||||
|
const preloadFetch = async (request) => {
|
||||||
|
const data = parsedInitialResults()
|
||||||
|
if (!data || !data[request]) {
|
||||||
|
return window.fetch(request)
|
||||||
|
}
|
||||||
|
const requestData = atob(data[request])
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
json: () => JSON.parse(requestData),
|
||||||
|
text: () => requestData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInstanceConfig = async ({ store }) => {
|
||||||
try {
|
try {
|
||||||
const res = await window.fetch('/api/statusnet/config.json')
|
const res = await preloadFetch('/api/v1/instance')
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey, safeDMMentionsEnabled } = data.site
|
const textlimit = data.max_toot_chars
|
||||||
|
const vapidPublicKey = data.pleroma.vapid_public_key
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', { name: 'name', value: name })
|
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
|
||||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'safeDM', value: safeDMMentionsEnabled !== '0' })
|
|
||||||
|
|
||||||
// TODO: default values for this stuff, added if to not make it break on
|
|
||||||
// my dev config out of the box.
|
|
||||||
if (uploadlimit) {
|
|
||||||
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadlimit.uploadlimit) })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadlimit.avatarlimit) })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadlimit.backgroundlimit) })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadlimit.bannerlimit) })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vapidPublicKey) {
|
if (vapidPublicKey) {
|
||||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.site.pleromafe
|
|
||||||
} else {
|
} else {
|
||||||
throw (res)
|
throw (res)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Could not load statusnet config, potentially fatal')
|
console.error('Could not load instance config, potentially fatal')
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBackendProvidedConfig = async ({ store }) => {
|
||||||
|
try {
|
||||||
|
const res = await window.fetch('/api/pleroma/frontend_configurations')
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
return data.pleroma_fe
|
||||||
|
} else {
|
||||||
|
throw (res)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Could not load backend-provided frontend config, potentially fatal')
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,7 +158,7 @@ const getTOS = async ({ store }) => {
|
||||||
|
|
||||||
const getInstancePanel = async ({ store }) => {
|
const getInstancePanel = async ({ store }) => {
|
||||||
try {
|
try {
|
||||||
const res = await window.fetch('/instance/panel.html')
|
const res = await preloadFetch('/instance/panel.html')
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const html = await res.text()
|
const html = await res.text()
|
||||||
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
|
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
|
||||||
|
@ -195,18 +221,28 @@ const resolveStaffAccounts = ({ store, accounts }) => {
|
||||||
|
|
||||||
const getNodeInfo = async ({ store }) => {
|
const getNodeInfo = async ({ store }) => {
|
||||||
try {
|
try {
|
||||||
const res = await window.fetch('/nodeinfo/2.0.json')
|
const res = await preloadFetch('/nodeinfo/2.0.json')
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
const metadata = data.metadata
|
const metadata = data.metadata
|
||||||
const features = metadata.features
|
const features = metadata.features
|
||||||
|
store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
|
||||||
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
||||||
store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
|
store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
|
||||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||||
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
||||||
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
||||||
|
|
||||||
|
const uploadLimits = metadata.uploadLimits
|
||||||
|
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits })
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
|
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
|
||||||
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
|
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
|
||||||
|
|
||||||
|
@ -257,7 +293,7 @@ const getNodeInfo = async ({ store }) => {
|
||||||
|
|
||||||
const setConfig = async ({ store }) => {
|
const setConfig = async ({ store }) => {
|
||||||
// apiConfig, staticConfig
|
// apiConfig, staticConfig
|
||||||
const configInfos = await Promise.all([getStatusnetConfig({ store }), getStaticConfig()])
|
const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()])
|
||||||
const apiConfig = configInfos[0]
|
const apiConfig = configInfos[0]
|
||||||
const staticConfig = configInfos[1]
|
const staticConfig = configInfos[1]
|
||||||
|
|
||||||
|
@ -280,6 +316,11 @@ const checkOAuthToken = async ({ store }) => {
|
||||||
const afterStoreSetup = async ({ store, i18n }) => {
|
const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
const width = windowWidth()
|
const width = windowWidth()
|
||||||
store.dispatch('setMobileLayout', width <= 800)
|
store.dispatch('setMobileLayout', width <= 800)
|
||||||
|
|
||||||
|
const overrides = window.___pleromafe_dev_overrides || {}
|
||||||
|
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
|
||||||
|
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||||
|
|
||||||
await setConfig({ store })
|
await setConfig({ store })
|
||||||
|
|
||||||
const { customTheme, customThemeSource } = store.state.config
|
const { customTheme, customThemeSource } = store.state.config
|
||||||
|
@ -299,16 +340,18 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we can try getting the server settings and logging in
|
// Now we can try getting the server settings and logging in
|
||||||
|
// Most of these are preloaded into the index.html so blocking is minimized
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
checkOAuthToken({ store }),
|
checkOAuthToken({ store }),
|
||||||
getTOS({ store }),
|
|
||||||
getInstancePanel({ store }),
|
getInstancePanel({ store }),
|
||||||
getStickers({ store }),
|
getNodeInfo({ store }),
|
||||||
getNodeInfo({ store })
|
getInstanceConfig({ store })
|
||||||
])
|
])
|
||||||
|
|
||||||
// Start fetching things that don't need to block the UI
|
// Start fetching things that don't need to block the UI
|
||||||
store.dispatch('fetchMutes')
|
store.dispatch('fetchMutes')
|
||||||
|
getTOS({ store })
|
||||||
|
getStickers({ store })
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
mode: 'history',
|
mode: 'history',
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<Popover
|
<Popover
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
|
:bound-to="{ x: 'container' }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
slot="content"
|
slot="content"
|
||||||
|
|
|
@ -431,6 +431,7 @@ const EmojiInput = {
|
||||||
const offsetBottom = offsetTop + offsetHeight
|
const offsetBottom = offsetTop + offsetHeight
|
||||||
|
|
||||||
panel.style.top = offsetBottom + 'px'
|
panel.style.top = offsetBottom + 'px'
|
||||||
|
if (!picker) return
|
||||||
picker.$el.style.top = offsetBottom + 'px'
|
picker.$el.style.top = offsetBottom + 'px'
|
||||||
picker.$el.style.bottom = 'auto'
|
picker.$el.style.bottom = 'auto'
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { debounce } from 'lodash'
|
||||||
|
|
||||||
const debounceUserSearch = debounce((data, input) => {
|
const debounceUserSearch = debounce((data, input) => {
|
||||||
data.updateUsersList(input)
|
data.updateUsersList(input)
|
||||||
}, 500, { leading: true, trailing: false })
|
}, 500)
|
||||||
|
|
||||||
export default data => input => {
|
export default data => input => {
|
||||||
const firstChar = input[0]
|
const firstChar = input[0]
|
||||||
|
@ -97,8 +97,8 @@ export const suggestUsers = data => input => {
|
||||||
replacement: '@' + screen_name + ' '
|
replacement: '@' + screen_name + ' '
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// BE search users if there are no matches
|
// BE search users to get more comprehensive results
|
||||||
if (newUsers.length === 0 && data.updateUsersList) {
|
if (data.updateUsersList) {
|
||||||
debounceUserSearch(data, noPrefix)
|
debounceUserSearch(data, noPrefix)
|
||||||
}
|
}
|
||||||
return newUsers
|
return newUsers
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="top"
|
placement="top"
|
||||||
class="extra-button-popover"
|
class="extra-button-popover"
|
||||||
|
:bound-to="{ x: 'container' }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
slot="content"
|
slot="content"
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<span class="result-percentage">
|
<span class="result-percentage">
|
||||||
{{ percentageForOption(option.votes_count) }}%
|
{{ percentageForOption(option.votes_count) }}%
|
||||||
</span>
|
</span>
|
||||||
<span>{{ option.title }}</span>
|
<span v-html="option.title_html"></span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="result-fill"
|
class="result-fill"
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
const Popover = {
|
const Popover = {
|
||||||
name: 'Popover',
|
name: 'Popover',
|
||||||
props: {
|
props: {
|
||||||
|
@ -10,6 +9,9 @@ const Popover = {
|
||||||
// 'container' for using offsetParent as boundaries for either axis
|
// 'container' for using offsetParent as boundaries for either axis
|
||||||
// or 'viewport'
|
// or 'viewport'
|
||||||
boundTo: Object,
|
boundTo: Object,
|
||||||
|
// Takes a selector to use as a replacement for the parent container
|
||||||
|
// for getting boundaries for x an y axis
|
||||||
|
boundToSelector: String,
|
||||||
// Takes a top/bottom/left/right object, how much space to leave
|
// Takes a top/bottom/left/right object, how much space to leave
|
||||||
// between boundary and popover element
|
// between boundary and popover element
|
||||||
margin: Object,
|
margin: Object,
|
||||||
|
@ -27,6 +29,10 @@ const Popover = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
containerBoundingClientRect () {
|
||||||
|
const container = this.boundToSelector ? this.$el.closest(this.boundToSelector) : this.$el.offsetParent
|
||||||
|
return container.getBoundingClientRect()
|
||||||
|
},
|
||||||
updateStyles () {
|
updateStyles () {
|
||||||
if (this.hidden) {
|
if (this.hidden) {
|
||||||
this.styles = {
|
this.styles = {
|
||||||
|
@ -45,7 +51,8 @@ const Popover = {
|
||||||
// Minor optimization, don't call a slow reflow call if we don't have to
|
// Minor optimization, don't call a slow reflow call if we don't have to
|
||||||
const parentBounds = this.boundTo &&
|
const parentBounds = this.boundTo &&
|
||||||
(this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
|
(this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
|
||||||
this.$el.offsetParent.getBoundingClientRect()
|
this.containerBoundingClientRect()
|
||||||
|
|
||||||
const margin = this.margin || {}
|
const margin = this.margin || {}
|
||||||
|
|
||||||
// What are the screen bounds for the popover? Viewport vs container
|
// What are the screen bounds for the popover? Viewport vs container
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import unescape from 'lodash/unescape'
|
import unescape from 'lodash/unescape'
|
||||||
|
import merge from 'lodash/merge'
|
||||||
import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
|
import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
|
||||||
import ScopeSelector from 'src/components/scope_selector/scope_selector.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 fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
|
||||||
|
@ -16,6 +17,7 @@ const ProfileTab = {
|
||||||
newLocked: this.$store.state.users.currentUser.locked,
|
newLocked: this.$store.state.users.currentUser.locked,
|
||||||
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
|
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
|
||||||
newDefaultScope: this.$store.state.users.currentUser.default_scope,
|
newDefaultScope: this.$store.state.users.currentUser.default_scope,
|
||||||
|
newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })),
|
||||||
hideFollows: this.$store.state.users.currentUser.hide_follows,
|
hideFollows: this.$store.state.users.currentUser.hide_follows,
|
||||||
hideFollowers: this.$store.state.users.currentUser.hide_followers,
|
hideFollowers: this.$store.state.users.currentUser.hide_followers,
|
||||||
hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
|
hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
|
||||||
|
@ -23,6 +25,7 @@ const ProfileTab = {
|
||||||
showRole: this.$store.state.users.currentUser.show_role,
|
showRole: this.$store.state.users.currentUser.show_role,
|
||||||
role: this.$store.state.users.currentUser.role,
|
role: this.$store.state.users.currentUser.role,
|
||||||
discoverable: this.$store.state.users.currentUser.discoverable,
|
discoverable: this.$store.state.users.currentUser.discoverable,
|
||||||
|
bot: this.$store.state.users.currentUser.bot,
|
||||||
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
||||||
pickAvatarBtnVisible: true,
|
pickAvatarBtnVisible: true,
|
||||||
bannerUploading: false,
|
bannerUploading: false,
|
||||||
|
@ -62,6 +65,18 @@ const ProfileTab = {
|
||||||
...this.$store.state.instance.emoji,
|
...this.$store.state.instance.emoji,
|
||||||
...this.$store.state.instance.customEmoji
|
...this.$store.state.instance.customEmoji
|
||||||
] })
|
] })
|
||||||
|
},
|
||||||
|
userSuggestor () {
|
||||||
|
return suggestor({
|
||||||
|
users: this.$store.state.users.users,
|
||||||
|
updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fieldsLimits () {
|
||||||
|
return this.$store.state.instance.fieldsLimits
|
||||||
|
},
|
||||||
|
maxFields () {
|
||||||
|
return this.fieldsLimits ? this.fieldsLimits.maxFields : 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -74,17 +89,21 @@ const ProfileTab = {
|
||||||
// Backend notation.
|
// Backend notation.
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
display_name: this.newName,
|
display_name: this.newName,
|
||||||
|
fields_attributes: this.newFields.filter(el => el != null),
|
||||||
default_scope: this.newDefaultScope,
|
default_scope: this.newDefaultScope,
|
||||||
no_rich_text: this.newNoRichText,
|
no_rich_text: this.newNoRichText,
|
||||||
hide_follows: this.hideFollows,
|
hide_follows: this.hideFollows,
|
||||||
hide_followers: this.hideFollowers,
|
hide_followers: this.hideFollowers,
|
||||||
discoverable: this.discoverable,
|
discoverable: this.discoverable,
|
||||||
|
bot: this.bot,
|
||||||
allow_following_move: this.allowFollowingMove,
|
allow_following_move: this.allowFollowingMove,
|
||||||
hide_follows_count: this.hideFollowsCount,
|
hide_follows_count: this.hideFollowsCount,
|
||||||
hide_followers_count: this.hideFollowersCount,
|
hide_followers_count: this.hideFollowersCount,
|
||||||
show_role: this.showRole
|
show_role: this.showRole
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
} }).then((user) => {
|
} }).then((user) => {
|
||||||
|
this.newFields.splice(user.fields.length)
|
||||||
|
merge(this.newFields, user.fields)
|
||||||
this.$store.commit('addNewUsers', [user])
|
this.$store.commit('addNewUsers', [user])
|
||||||
this.$store.commit('setCurrentUser', user)
|
this.$store.commit('setCurrentUser', user)
|
||||||
})
|
})
|
||||||
|
@ -92,6 +111,16 @@ const ProfileTab = {
|
||||||
changeVis (visibility) {
|
changeVis (visibility) {
|
||||||
this.newDefaultScope = visibility
|
this.newDefaultScope = visibility
|
||||||
},
|
},
|
||||||
|
addField () {
|
||||||
|
if (this.newFields.length < this.maxFields) {
|
||||||
|
this.newFields.push({ name: '', value: '' })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
deleteField (index, event) {
|
||||||
|
this.$delete(this.newFields, index)
|
||||||
|
},
|
||||||
uploadFile (slot, e) {
|
uploadFile (slot, e) {
|
||||||
const file = e.target.files[0]
|
const file = e.target.files[0]
|
||||||
if (!file) { return }
|
if (!file) { return }
|
||||||
|
|
|
@ -79,4 +79,21 @@
|
||||||
.setting-subitem {
|
.setting-subitem {
|
||||||
margin-left: 1.75em;
|
margin-left: 1.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-fields {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&>.emoji-input {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
margin: 0 .2em .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>.icon-container {
|
||||||
|
width: 20px;
|
||||||
|
|
||||||
|
&>.icon-cancel {
|
||||||
|
vertical-align: sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,6 +95,59 @@
|
||||||
{{ $t('settings.discoverable') }}
|
{{ $t('settings.discoverable') }}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</p>
|
</p>
|
||||||
|
<div v-if="maxFields > 0">
|
||||||
|
<p>{{ $t('settings.profile_fields.label') }}</p>
|
||||||
|
<div
|
||||||
|
v-for="(_, i) in newFields"
|
||||||
|
:key="i"
|
||||||
|
class="profile-fields"
|
||||||
|
>
|
||||||
|
<EmojiInput
|
||||||
|
v-model="newFields[i].name"
|
||||||
|
enable-emoji-picker
|
||||||
|
hide-emoji-button
|
||||||
|
:suggest="userSuggestor"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="newFields[i].name"
|
||||||
|
:placeholder="$t('settings.profile_fields.name')"
|
||||||
|
>
|
||||||
|
</EmojiInput>
|
||||||
|
<EmojiInput
|
||||||
|
v-model="newFields[i].value"
|
||||||
|
enable-emoji-picker
|
||||||
|
hide-emoji-button
|
||||||
|
:suggest="userSuggestor"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="newFields[i].value"
|
||||||
|
:placeholder="$t('settings.profile_fields.value')"
|
||||||
|
>
|
||||||
|
</EmojiInput>
|
||||||
|
<div
|
||||||
|
class="icon-container"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-show="newFields.length > 1"
|
||||||
|
class="icon-cancel"
|
||||||
|
@click="deleteField(i)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
v-if="newFields.length < maxFields"
|
||||||
|
class="add-field faint"
|
||||||
|
@click="addField"
|
||||||
|
>
|
||||||
|
<i class="icon-plus" />
|
||||||
|
{{ $t("settings.profile_fields.add_field") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<Checkbox v-model="bot">
|
||||||
|
{{ $t('settings.bot') }}
|
||||||
|
</Checkbox>
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
:disabled="newName && newName.length === 0"
|
:disabled="newName && newName.length === 0"
|
||||||
class="btn btn-default"
|
class="btn btn-default"
|
||||||
|
|
|
@ -418,7 +418,7 @@ $status-margin: 0.75em;
|
||||||
max-width: 85%;
|
max-width: 85%;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
img {
|
img.emoji {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
|
@ -164,11 +164,6 @@ $status-margin: 0.75em;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-content {
|
|
||||||
font-family: var(--postFont, sans-serif);
|
|
||||||
line-height: 1.4em;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
|
|
||||||
img, video {
|
img, video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
|
@ -181,6 +176,11 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-content {
|
||||||
|
font-family: var(--postFont, sans-serif);
|
||||||
|
line-height: 1.4em;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
margin: 0.2em 0 0.2em 2em;
|
margin: 0.2em 0 0.2em 2em;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|
|
@ -70,10 +70,20 @@
|
||||||
>
|
>
|
||||||
@{{ user.screen_name }}
|
@{{ user.screen_name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<template v-if="!hideBio">
|
||||||
<span
|
<span
|
||||||
v-if="!hideBio && !!visibleRole"
|
v-if="!!visibleRole"
|
||||||
class="alert staff"
|
class="alert user-role"
|
||||||
>{{ visibleRole }}</span>
|
>
|
||||||
|
{{ visibleRole }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="user.bot"
|
||||||
|
class="alert user-role"
|
||||||
|
>
|
||||||
|
bot
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
<span v-if="user.locked"><i class="icon icon-lock" /></span>
|
<span v-if="user.locked"><i class="icon icon-lock" /></span>
|
||||||
<span
|
<span
|
||||||
v-if="!mergedConfig.hideUserStats && !hideBio"
|
v-if="!mergedConfig.hideUserStats && !hideBio"
|
||||||
|
@ -458,7 +468,7 @@
|
||||||
color: var(--text, $fallback--text);
|
color: var(--text, $fallback--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.staff {
|
.user-role {
|
||||||
flex: none;
|
flex: none;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
|
|
|
@ -124,6 +124,14 @@ const UserProfile = {
|
||||||
onTabSwitch (tab) {
|
onTabSwitch (tab) {
|
||||||
this.tab = tab
|
this.tab = tab
|
||||||
this.$router.replace({ query: { tab } })
|
this.$router.replace({ query: { tab } })
|
||||||
|
},
|
||||||
|
linkClicked ({ target }) {
|
||||||
|
if (target.tagName === 'SPAN') {
|
||||||
|
target = target.parentNode
|
||||||
|
}
|
||||||
|
if (target.tagName === 'A') {
|
||||||
|
window.open(target.href, '_blank')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
@ -11,6 +11,31 @@
|
||||||
:allow-zooming-avatar="true"
|
:allow-zooming-avatar="true"
|
||||||
rounded="top"
|
rounded="top"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
v-if="user.fields_html && user.fields_html.length > 0"
|
||||||
|
class="user-profile-fields"
|
||||||
|
>
|
||||||
|
<dl
|
||||||
|
v-for="(field, index) in user.fields_html"
|
||||||
|
:key="index"
|
||||||
|
class="user-profile-field"
|
||||||
|
>
|
||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
<dt
|
||||||
|
:title="user.fields_text[index].name"
|
||||||
|
class="user-profile-field-name"
|
||||||
|
@click.prevent="linkClicked"
|
||||||
|
v-html="field.name"
|
||||||
|
/>
|
||||||
|
<dd
|
||||||
|
:title="user.fields_text[index].value"
|
||||||
|
class="user-profile-field-value"
|
||||||
|
@click.prevent="linkClicked"
|
||||||
|
v-html="field.value"
|
||||||
|
/>
|
||||||
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
<tab-switcher
|
<tab-switcher
|
||||||
:active-tab="tab"
|
:active-tab="tab"
|
||||||
:render-only-focused="true"
|
:render-only-focused="true"
|
||||||
|
@ -108,11 +133,60 @@
|
||||||
<script src="./user_profile.js"></script>
|
<script src="./user_profile.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.user-profile {
|
.user-profile {
|
||||||
flex: 2;
|
flex: 2;
|
||||||
flex-basis: 500px;
|
flex-basis: 500px;
|
||||||
|
|
||||||
|
.user-profile-fields {
|
||||||
|
margin: 0 0.5em;
|
||||||
|
img {
|
||||||
|
object-fit: contain;
|
||||||
|
vertical-align: middle;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 400px;
|
||||||
|
|
||||||
|
&.emoji {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile-field {
|
||||||
|
display: flex;
|
||||||
|
margin: 0.25em auto;
|
||||||
|
max-width: 32em;
|
||||||
|
border: 1px solid var(--border, $fallback--border);
|
||||||
|
border-radius: $fallback--inputRadius;
|
||||||
|
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||||
|
|
||||||
|
.user-profile-field-name {
|
||||||
|
flex: 0 1 30%;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: right;
|
||||||
|
color: var(--lightText);
|
||||||
|
min-width: 120px;
|
||||||
|
border-right: 1px solid var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile-field-value {
|
||||||
|
flex: 1 1 70%;
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0 0 0 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile-field-name, .user-profile-field-value {
|
||||||
|
line-height: 18px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0.5em 1.5em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.userlist-placeholder {
|
.userlist-placeholder {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -266,6 +266,7 @@
|
||||||
"block_import_error": "Error importing blocks",
|
"block_import_error": "Error importing blocks",
|
||||||
"blocks_imported": "Blocks imported! Processing them will take a while.",
|
"blocks_imported": "Blocks imported! Processing them will take a while.",
|
||||||
"blocks_tab": "Blocks",
|
"blocks_tab": "Blocks",
|
||||||
|
"bot": "This is a bot account",
|
||||||
"btnRadius": "Buttons",
|
"btnRadius": "Buttons",
|
||||||
"cBlue": "Blue (Reply, follow)",
|
"cBlue": "Blue (Reply, follow)",
|
||||||
"cGreen": "Green (Retweet)",
|
"cGreen": "Green (Retweet)",
|
||||||
|
@ -333,6 +334,12 @@
|
||||||
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
|
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
|
||||||
"mutes_tab": "Mutes",
|
"mutes_tab": "Mutes",
|
||||||
"play_videos_in_modal": "Play videos in a popup frame",
|
"play_videos_in_modal": "Play videos in a popup frame",
|
||||||
|
"profile_fields": {
|
||||||
|
"label": "Profile metadata",
|
||||||
|
"add_field": "Add Field",
|
||||||
|
"name": "Label",
|
||||||
|
"value": "Content"
|
||||||
|
},
|
||||||
"use_contain_fit": "Don't crop the attachment in thumbnails",
|
"use_contain_fit": "Don't crop the attachment in thumbnails",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_bio": "Name & Bio",
|
"name_bio": "Name & Bio",
|
||||||
|
|
|
@ -255,7 +255,8 @@
|
||||||
"top_bar": "Barra superiore",
|
"top_bar": "Barra superiore",
|
||||||
"panel_header": "Titolo pannello",
|
"panel_header": "Titolo pannello",
|
||||||
"badge_notification": "Notifica",
|
"badge_notification": "Notifica",
|
||||||
"popover": "Suggerimenti, menù, sbalzi"
|
"popover": "Suggerimenti, menù, sbalzi",
|
||||||
|
"toggled": "Scambiato"
|
||||||
},
|
},
|
||||||
"common_colors": {
|
"common_colors": {
|
||||||
"rgbo": "Icone, accenti, medaglie",
|
"rgbo": "Icone, accenti, medaglie",
|
||||||
|
|
|
@ -28,7 +28,12 @@
|
||||||
"enable": "Inschakelen",
|
"enable": "Inschakelen",
|
||||||
"confirm": "Bevestigen",
|
"confirm": "Bevestigen",
|
||||||
"verify": "Verifiëren",
|
"verify": "Verifiëren",
|
||||||
"generic_error": "Er is een fout opgetreden"
|
"generic_error": "Er is een fout opgetreden",
|
||||||
|
"peek": "Spiek",
|
||||||
|
"close": "Sluiten",
|
||||||
|
"retry": "Opnieuw proberen",
|
||||||
|
"error_retry": "Probeer het opnieuw",
|
||||||
|
"loading": "Laden…"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"login": "Log in",
|
"login": "Log in",
|
||||||
|
@ -90,7 +95,7 @@
|
||||||
"text/bbcode": "BBCode"
|
"text/bbcode": "BBCode"
|
||||||
},
|
},
|
||||||
"content_warning": "Onderwerp (optioneel)",
|
"content_warning": "Onderwerp (optioneel)",
|
||||||
"default": "Zojuist geland in L.A.",
|
"default": "Tijd voor anime!",
|
||||||
"direct_warning": "Deze post zal enkel zichtbaar zijn voor de personen die genoemd zijn.",
|
"direct_warning": "Deze post zal enkel zichtbaar zijn voor de personen die genoemd zijn.",
|
||||||
"posting": "Plaatsen",
|
"posting": "Plaatsen",
|
||||||
"scope": {
|
"scope": {
|
||||||
|
@ -377,7 +382,7 @@
|
||||||
"button": "Knop",
|
"button": "Knop",
|
||||||
"text": "Nog een boel andere {0} en {1}",
|
"text": "Nog een boel andere {0} en {1}",
|
||||||
"mono": "inhoud",
|
"mono": "inhoud",
|
||||||
"input": "Zojuist geland in L.A.",
|
"input": "Tijd voor anime!",
|
||||||
"faint_link": "handige gebruikershandleiding",
|
"faint_link": "handige gebruikershandleiding",
|
||||||
"fine_print": "Lees onze {0} om niets nuttig te leren!",
|
"fine_print": "Lees onze {0} om niets nuttig te leren!",
|
||||||
"header_faint": "Alles komt goed",
|
"header_faint": "Alles komt goed",
|
||||||
|
@ -451,7 +456,7 @@
|
||||||
"user_mutes": "Gebruikers",
|
"user_mutes": "Gebruikers",
|
||||||
"useStreamingApi": "Berichten en meldingen in real-time ontvangen",
|
"useStreamingApi": "Berichten en meldingen in real-time ontvangen",
|
||||||
"useStreamingApiWarning": "(Afgeraden, experimenteel, kan berichten overslaan)",
|
"useStreamingApiWarning": "(Afgeraden, experimenteel, kan berichten overslaan)",
|
||||||
"type_domains_to_mute": "Voer domeinen in om te negeren",
|
"type_domains_to_mute": "Zoek domeinen om te negeren",
|
||||||
"upload_a_photo": "Upload een foto",
|
"upload_a_photo": "Upload een foto",
|
||||||
"fun": "Plezier",
|
"fun": "Plezier",
|
||||||
"greentext": "Meme pijlen",
|
"greentext": "Meme pijlen",
|
||||||
|
@ -470,7 +475,15 @@
|
||||||
"frontend_version": "Frontend Versie",
|
"frontend_version": "Frontend Versie",
|
||||||
"backend_version": "Backend Versie",
|
"backend_version": "Backend Versie",
|
||||||
"title": "Versie"
|
"title": "Versie"
|
||||||
}
|
},
|
||||||
|
"mutes_and_blocks": "Negeringen en Blokkades",
|
||||||
|
"profile_fields": {
|
||||||
|
"value": "Inhoud",
|
||||||
|
"name": "Label",
|
||||||
|
"add_field": "Veld Toevoegen",
|
||||||
|
"label": "Profiel metadata"
|
||||||
|
},
|
||||||
|
"bot": "Dit is een bot account"
|
||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"collapse": "Inklappen",
|
"collapse": "Inklappen",
|
||||||
|
@ -708,7 +721,9 @@
|
||||||
"unpin": "Van profiel losmaken",
|
"unpin": "Van profiel losmaken",
|
||||||
"delete": "Status verwijderen",
|
"delete": "Status verwijderen",
|
||||||
"repeats": "Herhalingen",
|
"repeats": "Herhalingen",
|
||||||
"favorites": "Favorieten"
|
"favorites": "Favorieten",
|
||||||
|
"thread_muted_and_words": ", heeft woorden:",
|
||||||
|
"thread_muted": "Thread genegeerd"
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
"years_short": "{0}j",
|
"years_short": "{0}j",
|
||||||
|
|
|
@ -130,6 +130,7 @@
|
||||||
"background": "Фон",
|
"background": "Фон",
|
||||||
"bio": "Описание",
|
"bio": "Описание",
|
||||||
"btnRadius": "Кнопки",
|
"btnRadius": "Кнопки",
|
||||||
|
"bot": "Это аккаунт бота",
|
||||||
"cBlue": "Ответить, читать",
|
"cBlue": "Ответить, читать",
|
||||||
"cGreen": "Повторить",
|
"cGreen": "Повторить",
|
||||||
"cOrange": "Нравится",
|
"cOrange": "Нравится",
|
||||||
|
|
35
src/i18n/service_worker_messages.js
Normal file
35
src/i18n/service_worker_messages.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/* eslint-disable import/no-webpack-loader-syntax */
|
||||||
|
// This module exports only the notification part of the i18n,
|
||||||
|
// which is useful for the service worker
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
ar: require('../lib/notification-i18n-loader.js!./ar.json'),
|
||||||
|
ca: require('../lib/notification-i18n-loader.js!./ca.json'),
|
||||||
|
cs: require('../lib/notification-i18n-loader.js!./cs.json'),
|
||||||
|
de: require('../lib/notification-i18n-loader.js!./de.json'),
|
||||||
|
eo: require('../lib/notification-i18n-loader.js!./eo.json'),
|
||||||
|
es: require('../lib/notification-i18n-loader.js!./es.json'),
|
||||||
|
et: require('../lib/notification-i18n-loader.js!./et.json'),
|
||||||
|
eu: require('../lib/notification-i18n-loader.js!./eu.json'),
|
||||||
|
fi: require('../lib/notification-i18n-loader.js!./fi.json'),
|
||||||
|
fr: require('../lib/notification-i18n-loader.js!./fr.json'),
|
||||||
|
ga: require('../lib/notification-i18n-loader.js!./ga.json'),
|
||||||
|
he: require('../lib/notification-i18n-loader.js!./he.json'),
|
||||||
|
hu: require('../lib/notification-i18n-loader.js!./hu.json'),
|
||||||
|
it: require('../lib/notification-i18n-loader.js!./it.json'),
|
||||||
|
ja: require('../lib/notification-i18n-loader.js!./ja_pedantic.json'),
|
||||||
|
ja_easy: require('../lib/notification-i18n-loader.js!./ja_easy.json'),
|
||||||
|
ko: require('../lib/notification-i18n-loader.js!./ko.json'),
|
||||||
|
nb: require('../lib/notification-i18n-loader.js!./nb.json'),
|
||||||
|
nl: require('../lib/notification-i18n-loader.js!./nl.json'),
|
||||||
|
oc: require('../lib/notification-i18n-loader.js!./oc.json'),
|
||||||
|
pl: require('../lib/notification-i18n-loader.js!./pl.json'),
|
||||||
|
pt: require('../lib/notification-i18n-loader.js!./pt.json'),
|
||||||
|
ro: require('../lib/notification-i18n-loader.js!./ro.json'),
|
||||||
|
ru: require('../lib/notification-i18n-loader.js!./ru.json'),
|
||||||
|
te: require('../lib/notification-i18n-loader.js!./te.json'),
|
||||||
|
zh: require('../lib/notification-i18n-loader.js!./zh.json'),
|
||||||
|
en: require('../lib/notification-i18n-loader.js!./en.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
export default messages
|
12
src/lib/notification-i18n-loader.js
Normal file
12
src/lib/notification-i18n-loader.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// This somewhat mysterious module will load a json string
|
||||||
|
// and then extract only the 'notifications' part. This is
|
||||||
|
// meant to be used to load the partial i18n we need for
|
||||||
|
// the service worker.
|
||||||
|
module.exports = function (source) {
|
||||||
|
var object = JSON.parse(source)
|
||||||
|
var smol = {
|
||||||
|
notifications: object.notifications || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(smol)
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ import {
|
||||||
omitBy
|
omitBy
|
||||||
} from 'lodash'
|
} from 'lodash'
|
||||||
import { set } from 'vue'
|
import { set } from 'vue'
|
||||||
import { isStatusNotification } from '../services/notification_utils/notification_utils.js'
|
import { isStatusNotification, prepareNotificationObject } from '../services/notification_utils/notification_utils.js'
|
||||||
import apiService from '../services/api/api.service.js'
|
import apiService from '../services/api/api.service.js'
|
||||||
import { muteWordHits } from '../services/status_parser/status_parser.js'
|
import { muteWordHits } from '../services/status_parser/status_parser.js'
|
||||||
|
|
||||||
|
@ -344,42 +344,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
||||||
state.notifications.idStore[notification.id] = notification
|
state.notifications.idStore[notification.id] = notification
|
||||||
|
|
||||||
if ('Notification' in window && window.Notification.permission === 'granted') {
|
if ('Notification' in window && window.Notification.permission === 'granted') {
|
||||||
const notifObj = {}
|
const notifObj = prepareNotificationObject(notification, rootGetters.i18n)
|
||||||
const status = notification.status
|
|
||||||
const title = notification.from_profile.name
|
|
||||||
notifObj.icon = notification.from_profile.profile_image_url
|
|
||||||
let i18nString
|
|
||||||
switch (notification.type) {
|
|
||||||
case 'like':
|
|
||||||
i18nString = 'favorited_you'
|
|
||||||
break
|
|
||||||
case 'repeat':
|
|
||||||
i18nString = 'repeated_you'
|
|
||||||
break
|
|
||||||
case 'follow':
|
|
||||||
i18nString = 'followed_you'
|
|
||||||
break
|
|
||||||
case 'move':
|
|
||||||
i18nString = 'migrated_to'
|
|
||||||
break
|
|
||||||
case 'follow_request':
|
|
||||||
i18nString = 'follow_request'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notification.type === 'pleroma:emoji_reaction') {
|
|
||||||
notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji])
|
|
||||||
} else if (i18nString) {
|
|
||||||
notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
|
|
||||||
} else if (isStatusNotification(notification.type)) {
|
|
||||||
notifObj.body = notification.status.text
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
|
|
||||||
if (status && status.attachments && status.attachments.length > 0 && !status.nsfw &&
|
|
||||||
status.attachments[0].mimetype.startsWith('image/')) {
|
|
||||||
notifObj.image = status.attachments[0].url
|
|
||||||
}
|
|
||||||
|
|
||||||
const reasonsToMuteNotif = (
|
const reasonsToMuteNotif = (
|
||||||
notification.seen ||
|
notification.seen ||
|
||||||
|
@ -393,7 +358,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (!reasonsToMuteNotif) {
|
if (!reasonsToMuteNotif) {
|
||||||
let desktopNotification = new window.Notification(title, notifObj)
|
let desktopNotification = new window.Notification(notifObj.title, notifObj)
|
||||||
// Chrome is known for not closing notifications automatically
|
// Chrome is known for not closing notifications automatically
|
||||||
// according to MDN, anyway.
|
// according to MDN, anyway.
|
||||||
setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
|
setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import oauthApi from '../services/new_api/oauth.js'
|
import oauthApi from '../services/new_api/oauth.js'
|
||||||
import { compact, map, each, merge, last, concat, uniq } from 'lodash'
|
import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
|
||||||
import { set } from 'vue'
|
import { set } from 'vue'
|
||||||
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
|
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ export const mergeOrAdd = (arr, obj, item) => {
|
||||||
const oldItem = obj[item.id]
|
const oldItem = obj[item.id]
|
||||||
if (oldItem) {
|
if (oldItem) {
|
||||||
// We already have this, so only merge the new info.
|
// We already have this, so only merge the new info.
|
||||||
merge(oldItem, item)
|
mergeWith(oldItem, item, mergeArrayLength)
|
||||||
return { item: oldItem, new: false }
|
return { item: oldItem, new: false }
|
||||||
} else {
|
} else {
|
||||||
// This is a new item, prepare it
|
// This is a new item, prepare it
|
||||||
|
@ -23,6 +23,13 @@ export const mergeOrAdd = (arr, obj, item) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mergeArrayLength = (oldValue, newValue) => {
|
||||||
|
if (isArray(oldValue) && isArray(newValue)) {
|
||||||
|
oldValue.length = newValue.length
|
||||||
|
return mergeWith(oldValue, newValue, mergeArrayLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getNotificationPermission = () => {
|
const getNotificationPermission = () => {
|
||||||
const Notification = window.Notification
|
const Notification = window.Notification
|
||||||
|
|
||||||
|
@ -116,7 +123,7 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
setCurrentUser (state, user) {
|
setCurrentUser (state, user) {
|
||||||
state.lastLoginName = user.screen_name
|
state.lastLoginName = user.screen_name
|
||||||
state.currentUser = merge(state.currentUser || {}, user)
|
state.currentUser = mergeWith(state.currentUser || {}, user, mergeArrayLength)
|
||||||
},
|
},
|
||||||
clearCurrentUser (state) {
|
clearCurrentUser (state) {
|
||||||
state.currentUser = false
|
state.currentUser = false
|
||||||
|
@ -428,10 +435,10 @@ const users = {
|
||||||
store.commit('setUserForNotification', notification)
|
store.commit('setUserForNotification', notification)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
searchUsers (store, { query }) {
|
searchUsers ({ rootState, commit }, { query }) {
|
||||||
return store.rootState.api.backendInteractor.searchUsers({ query })
|
return rootState.api.backendInteractor.searchUsers({ query })
|
||||||
.then((users) => {
|
.then((users) => {
|
||||||
store.commit('addNewUsers', users)
|
commit('addNewUsers', users)
|
||||||
return users
|
return users
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -56,6 +56,12 @@ export const parseUser = (data) => {
|
||||||
value: addEmojis(field.value, data.emojis)
|
value: addEmojis(field.value, data.emojis)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
output.fields_text = data.fields.map(field => {
|
||||||
|
return {
|
||||||
|
name: unescape(field.name.replace(/<[^>]*>/g, '')),
|
||||||
|
value: unescape(field.value.replace(/<[^>]*>/g, ''))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Utilize avatar_static for gif avatars?
|
// Utilize avatar_static for gif avatars?
|
||||||
output.profile_image_url = data.avatar
|
output.profile_image_url = data.avatar
|
||||||
|
@ -258,6 +264,12 @@ export const parseStatus = (data) => {
|
||||||
output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis)
|
output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis)
|
||||||
output.external_url = data.url
|
output.external_url = data.url
|
||||||
output.poll = data.poll
|
output.poll = data.poll
|
||||||
|
if (output.poll) {
|
||||||
|
output.poll.options = (output.poll.options || []).map(field => ({
|
||||||
|
...field,
|
||||||
|
title_html: addEmojis(field.title, data.emojis)
|
||||||
|
}))
|
||||||
|
}
|
||||||
output.pinned = data.pinned
|
output.pinned = data.pinned
|
||||||
output.muted = data.muted
|
output.muted = data.muted
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -43,3 +43,47 @@ export const filteredNotificationsFromStore = (store, types) => {
|
||||||
|
|
||||||
export const unseenNotificationsFromStore = store =>
|
export const unseenNotificationsFromStore = store =>
|
||||||
filter(filteredNotificationsFromStore(store), ({ seen }) => !seen)
|
filter(filteredNotificationsFromStore(store), ({ seen }) => !seen)
|
||||||
|
|
||||||
|
export const prepareNotificationObject = (notification, i18n) => {
|
||||||
|
const notifObj = {
|
||||||
|
tag: notification.id
|
||||||
|
}
|
||||||
|
const status = notification.status
|
||||||
|
const title = notification.from_profile.name
|
||||||
|
notifObj.title = title
|
||||||
|
notifObj.icon = notification.from_profile.profile_image_url
|
||||||
|
let i18nString
|
||||||
|
switch (notification.type) {
|
||||||
|
case 'like':
|
||||||
|
i18nString = 'favorited_you'
|
||||||
|
break
|
||||||
|
case 'repeat':
|
||||||
|
i18nString = 'repeated_you'
|
||||||
|
break
|
||||||
|
case 'follow':
|
||||||
|
i18nString = 'followed_you'
|
||||||
|
break
|
||||||
|
case 'move':
|
||||||
|
i18nString = 'migrated_to'
|
||||||
|
break
|
||||||
|
case 'follow_request':
|
||||||
|
i18nString = 'follow_request'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification.type === 'pleroma:emoji_reaction') {
|
||||||
|
notifObj.body = i18n.t('notifications.reacted_with', [notification.emoji])
|
||||||
|
} else if (i18nString) {
|
||||||
|
notifObj.body = i18n.t('notifications.' + i18nString)
|
||||||
|
} else if (isStatusNotification(notification.type)) {
|
||||||
|
notifObj.body = notification.status.text
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
|
||||||
|
if (status && status.attachments && status.attachments.length > 0 && !status.nsfw &&
|
||||||
|
status.attachments[0].mimetype.startsWith('image/')) {
|
||||||
|
notifObj.image = status.attachments[0].url
|
||||||
|
}
|
||||||
|
|
||||||
|
return notifObj
|
||||||
|
}
|
||||||
|
|
45
src/sw.js
45
src/sw.js
|
@ -1,6 +1,19 @@
|
||||||
/* eslint-env serviceworker */
|
/* eslint-env serviceworker */
|
||||||
|
|
||||||
import localForage from 'localforage'
|
import localForage from 'localforage'
|
||||||
|
import { parseNotification } from './services/entity_normalizer/entity_normalizer.service.js'
|
||||||
|
import { prepareNotificationObject } from './services/notification_utils/notification_utils.js'
|
||||||
|
import Vue from 'vue'
|
||||||
|
import VueI18n from 'vue-i18n'
|
||||||
|
import messages from './i18n/service_worker_messages.js'
|
||||||
|
|
||||||
|
Vue.use(VueI18n)
|
||||||
|
const i18n = new VueI18n({
|
||||||
|
// By default, use the browser locale, we will update it if neccessary
|
||||||
|
locale: 'en',
|
||||||
|
fallbackLocale: 'en',
|
||||||
|
messages
|
||||||
|
})
|
||||||
|
|
||||||
function isEnabled () {
|
function isEnabled () {
|
||||||
return localForage.getItem('vuex-lz')
|
return localForage.getItem('vuex-lz')
|
||||||
|
@ -12,15 +25,33 @@ function getWindowClients () {
|
||||||
.then((clientList) => clientList.filter(({ type }) => type === 'window'))
|
.then((clientList) => clientList.filter(({ type }) => type === 'window'))
|
||||||
}
|
}
|
||||||
|
|
||||||
self.addEventListener('push', (event) => {
|
const setLocale = async () => {
|
||||||
if (event.data) {
|
const state = await localForage.getItem('vuex-lz')
|
||||||
event.waitUntil(isEnabled().then((isEnabled) => {
|
const locale = state.config.interfaceLanguage || 'en'
|
||||||
return isEnabled && getWindowClients().then((list) => {
|
i18n.locale = locale
|
||||||
|
}
|
||||||
|
|
||||||
|
const maybeShowNotification = async (event) => {
|
||||||
|
const enabled = await isEnabled()
|
||||||
|
const activeClients = await getWindowClients()
|
||||||
|
await setLocale()
|
||||||
|
if (enabled && (activeClients.length === 0)) {
|
||||||
const data = event.data.json()
|
const data = event.data.json()
|
||||||
|
|
||||||
if (list.length === 0) return self.registration.showNotification(data.title, data)
|
const url = `${self.registration.scope}api/v1/notifications/${data.notification_id}`
|
||||||
})
|
const notification = await fetch(url, { headers: { Authorization: 'Bearer ' + data.access_token } })
|
||||||
}))
|
const notificationJson = await notification.json()
|
||||||
|
const parsedNotification = parseNotification(notificationJson)
|
||||||
|
|
||||||
|
const res = prepareNotificationObject(parsedNotification, i18n)
|
||||||
|
|
||||||
|
self.registration.showNotification(res.title, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('push', async (event) => {
|
||||||
|
if (event.data) {
|
||||||
|
event.waitUntil(maybeShowNotification(event))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,42 @@ describe('The users module', () => {
|
||||||
expect(state.users).to.eql([user])
|
expect(state.users).to.eql([user])
|
||||||
expect(state.users[0].name).to.eql('Dude')
|
expect(state.users[0].name).to.eql('Dude')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('merging array field in new information for old users', () => {
|
||||||
|
const state = cloneDeep(defaultState)
|
||||||
|
const user = {
|
||||||
|
id: '1',
|
||||||
|
fields: [
|
||||||
|
{ name: 'Label 1', value: 'Content 1' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const firstModUser = {
|
||||||
|
id: '1',
|
||||||
|
fields: [
|
||||||
|
{ name: 'Label 2', value: 'Content 2' },
|
||||||
|
{ name: 'Label 3', value: 'Content 3' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const secondModUser = {
|
||||||
|
id: '1',
|
||||||
|
fields: [
|
||||||
|
{ name: 'Label 4', value: 'Content 4' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
mutations.addNewUsers(state, [user])
|
||||||
|
expect(state.users[0].fields).to.have.length(1)
|
||||||
|
expect(state.users[0].fields[0].name).to.eql('Label 1')
|
||||||
|
|
||||||
|
mutations.addNewUsers(state, [firstModUser])
|
||||||
|
expect(state.users[0].fields).to.have.length(2)
|
||||||
|
expect(state.users[0].fields[0].name).to.eql('Label 2')
|
||||||
|
expect(state.users[0].fields[1].name).to.eql('Label 3')
|
||||||
|
|
||||||
|
mutations.addNewUsers(state, [secondModUser])
|
||||||
|
expect(state.users[0].fields).to.have.length(1)
|
||||||
|
expect(state.users[0].fields[0].name).to.eql('Label 4')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('findUser', () => {
|
describe('findUser', () => {
|
||||||
|
|
|
@ -290,6 +290,19 @@ describe('API Entities normalizer', () => {
|
||||||
expect(field).to.have.property('value').that.contains('<img')
|
expect(field).to.have.property('value').that.contains('<img')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('removes html tags from user profile fields', () => {
|
||||||
|
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: 'user', value: '<a rel="me" href="https://example.com/@user">@user</a>' }] })
|
||||||
|
|
||||||
|
const parsedUser = parseUser(user)
|
||||||
|
|
||||||
|
expect(parsedUser).to.have.property('fields_text').to.be.an('array')
|
||||||
|
|
||||||
|
const field = parsedUser.fields_text[0]
|
||||||
|
|
||||||
|
expect(field).to.have.property('name').that.equal('user')
|
||||||
|
expect(field).to.have.property('value').that.equal('@user')
|
||||||
|
})
|
||||||
|
|
||||||
it('adds hide_follows and hide_followers user settings', () => {
|
it('adds hide_follows and hide_followers user settings', () => {
|
||||||
const user = makeMockUserMasto({ pleroma: { hide_followers: true, hide_follows: false, hide_followers_count: false, hide_follows_count: true } })
|
const user = makeMockUserMasto({ pleroma: { hide_followers: true, hide_follows: false, hide_followers_count: false, hide_follows_count: true } })
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue