Merge remote-tracking branch 'origin/develop' into scrolltotop
* origin/develop: (59 commits) a11y Use dedicated indicator for non-ascii domain names add a favorites "timeline" shortcut refactor navigation-entry and use them in other nav items Update dependency sinon-chai to v3 Update dependency semver to v7 Update dependency vue-router to v4.1.5 Update dependency eslint to v8.23.0 Update dependency vue-template-compiler to v2.7.10 Update dependency @vue/babel-helper-vue-jsx-merge-props to v1.4.0 Update dependency eslint-plugin-promise to v6.0.1 fix lists edit page change ugly checkbox to a list element that doesn't look too much out of place a11y squeeze/stretch pinned items as long as there's enough space for it, hide items that won't fitc Remove isparta lint fix being unable to edit timeline pins on mobile aria fix mobile side drawer causing issues ...
This commit is contained in:
commit
887fac5add
80 changed files with 1974 additions and 1308 deletions
17
package.json
17
package.json
|
@ -43,19 +43,19 @@
|
|||
"utf8": "3.0.0",
|
||||
"vue": "3.2.37",
|
||||
"vue-i18n": "9.2.2",
|
||||
"vue-router": "4.1.3",
|
||||
"vue-template-compiler": "2.7.9",
|
||||
"vue-router": "4.1.5",
|
||||
"vue-template-compiler": "2.7.10",
|
||||
"vuex": "4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.18.10",
|
||||
"@babel/core": "7.18.13",
|
||||
"@babel/eslint-parser": "7.18.9",
|
||||
"@babel/plugin-transform-runtime": "7.18.10",
|
||||
"@babel/preset-env": "7.18.10",
|
||||
"@babel/register": "7.18.9",
|
||||
"@intlify/vue-i18n-loader": "5.0.0",
|
||||
"@ungap/event-target": "0.2.3",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "1.2.1",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
|
||||
"@vue/babel-plugin-jsx": "1.1.1",
|
||||
"@vue/compiler-sfc": "3.2.37",
|
||||
"@vue/test-utils": "2.0.2",
|
||||
|
@ -71,12 +71,12 @@
|
|||
"css-loader": "6.7.1",
|
||||
"css-minimizer-webpack-plugin": "4.0.0",
|
||||
"custom-event-polyfill": "1.0.7",
|
||||
"eslint": "8.22.0",
|
||||
"eslint": "8.23.0",
|
||||
"eslint-config-standard": "17.0.0",
|
||||
"eslint-formatter-friendly": "7.0.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-n": "15.2.5",
|
||||
"eslint-plugin-promise": "6.0.0",
|
||||
"eslint-plugin-promise": "6.0.1",
|
||||
"eslint-plugin-vue": "9.4.0",
|
||||
"eslint-webpack-plugin": "3.2.0",
|
||||
"eventsource-polyfill": "0.9.6",
|
||||
|
@ -85,7 +85,6 @@
|
|||
"html-webpack-plugin": "5.5.0",
|
||||
"http-proxy-middleware": "2.0.6",
|
||||
"iso-639-1": "2.1.15",
|
||||
"isparta-loader": "2.0.0",
|
||||
"json-loader": "0.5.7",
|
||||
"karma": "6.4.0",
|
||||
"karma-coverage": "2.2.0",
|
||||
|
@ -108,11 +107,11 @@
|
|||
"sass": "1.54.5",
|
||||
"sass-loader": "13.0.2",
|
||||
"selenium-server": "2.53.1",
|
||||
"semver": "5.7.1",
|
||||
"semver": "7.3.7",
|
||||
"serviceworker-webpack5-plugin": "2.0.0",
|
||||
"shelljs": "0.8.5",
|
||||
"sinon": "2.4.1",
|
||||
"sinon-chai": "2.14.0",
|
||||
"sinon-chai": "3.7.0",
|
||||
"stylelint": "13.13.1",
|
||||
"stylelint-config-standard": "20.0.0",
|
||||
"stylelint-rscss": "0.4.0",
|
||||
|
|
|
@ -92,8 +92,12 @@ export default {
|
|||
isChats () {
|
||||
return this.$route.name === 'chat' || this.$route.name === 'chats'
|
||||
},
|
||||
isListEdit () {
|
||||
return this.$route.name === 'lists-edit'
|
||||
},
|
||||
newPostButtonShown () {
|
||||
if (this.isChats) return false
|
||||
if (this.isListEdit) return false
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
||||
},
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
|
|
26
src/App.scss
26
src/App.scss
|
@ -117,12 +117,28 @@ h4 {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.iconLetter {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
font-weight: 1000;
|
||||
}
|
||||
|
||||
i[class*=icon-],
|
||||
.svg-inline--fa {
|
||||
.svg-inline--fa,
|
||||
.iconLetter {
|
||||
color: $fallback--icon;
|
||||
color: var(--icon, $fallback--icon);
|
||||
}
|
||||
|
||||
.button-unstyled:hover,
|
||||
a:hover {
|
||||
> i[class*=icon-],
|
||||
> .svg-inline--fa,
|
||||
> .iconLetter {
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
z-index: var(--ZI_navbar);
|
||||
color: var(--topBarText);
|
||||
|
@ -765,17 +781,23 @@ option {
|
|||
}
|
||||
|
||||
.fa-scale-110 {
|
||||
&.svg-inline--fa {
|
||||
&.svg-inline--fa,
|
||||
&.iconLetter {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
|
||||
.fa-old-padding {
|
||||
&.iconLetter,
|
||||
&.svg-inline--fa, &-layer {
|
||||
padding: 0 0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.veryfaint {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.login-hint {
|
||||
text-align: center;
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
<div
|
||||
id="main-scroller"
|
||||
class="column main"
|
||||
:class="{ '-full-height': isChats }"
|
||||
:class="{ '-full-height': isChats || isListEdit }"
|
||||
>
|
||||
<div
|
||||
v-if="!currentUser"
|
||||
|
|
|
@ -23,6 +23,7 @@ import RemoteUserResolver from 'components/remote_user_resolver/remote_user_reso
|
|||
import Lists from 'components/lists/lists.vue'
|
||||
import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
|
||||
import ListsEdit from 'components/lists_edit/lists_edit.vue'
|
||||
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
||||
|
||||
export default (store) => {
|
||||
const validateAuthenticatedRoute = (to, from, next) => {
|
||||
|
@ -79,7 +80,9 @@ export default (store) => {
|
|||
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile },
|
||||
{ name: 'lists', path: '/lists', component: Lists },
|
||||
{ name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline },
|
||||
{ name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit }
|
||||
{ name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit },
|
||||
{ name: 'lists-new', path: '/lists/new', component: ListsEdit },
|
||||
{ name: 'edit-navigation', path: '/nav-edit', component: NavPanel, props: () => ({ forceExpand: true, forceEditMode: true }), beforeEnter: validateAuthenticatedRoute }
|
||||
]
|
||||
|
||||
if (store.state.instance.pleromaChatMessagesAvailable) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { mapState } from 'vuex'
|
||||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faEllipsisV
|
||||
|
@ -19,7 +20,8 @@ const AccountActions = {
|
|||
},
|
||||
components: {
|
||||
ProgressButton,
|
||||
Popover
|
||||
Popover,
|
||||
UserListMenu
|
||||
},
|
||||
methods: {
|
||||
showRepeats () {
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
class="dropdown-divider"
|
||||
/>
|
||||
</template>
|
||||
<UserListMenu :user="user" />
|
||||
<button
|
||||
v-if="relationship.blocking"
|
||||
class="btn button-default btn-block dropdown-item"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import UserPopover from '../user_popover/user_popover.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserLink from '../user_link/user_link.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
|
@ -10,7 +11,8 @@ const BasicUserCard = {
|
|||
components: {
|
||||
UserPopover,
|
||||
UserAvatar,
|
||||
RichContent
|
||||
RichContent,
|
||||
UserLink
|
||||
},
|
||||
methods: {
|
||||
userProfileLink (user) {
|
||||
|
|
|
@ -30,12 +30,10 @@
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<router-link
|
||||
<user-link
|
||||
class="basic-user-card-screen-name"
|
||||
:to="userProfileLink(user)"
|
||||
>
|
||||
@{{ user.screen_name_ui }}
|
||||
</router-link>
|
||||
:user="user"
|
||||
/>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
|
|
|
@ -137,4 +137,8 @@
|
|||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
:title="$t('nav.administration')"
|
||||
/>
|
||||
</a>
|
||||
<span class="spacer" />
|
||||
<button
|
||||
v-if="currentUser"
|
||||
class="button-unstyled nav-icon"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Completion from '../../services/completion/completion.js'
|
||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||
import { take } from 'lodash'
|
||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||
|
||||
|
@ -120,7 +121,8 @@ const EmojiInput = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
EmojiPicker
|
||||
EmojiPicker,
|
||||
UnicodeDomainIndicator
|
||||
},
|
||||
computed: {
|
||||
padEmoji () {
|
||||
|
|
|
@ -50,7 +50,21 @@
|
|||
<span v-else>{{ suggestion.replacement }}</span>
|
||||
</span>
|
||||
<div class="label">
|
||||
<span class="displayText">{{ suggestion.displayText }}</span>
|
||||
<span
|
||||
v-if="suggestion.user"
|
||||
class="displayText"
|
||||
>
|
||||
{{ suggestion.displayText }}<UnicodeDomainIndicator
|
||||
:user="suggestion.user"
|
||||
:at="false"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
v-if="!suggestion.user"
|
||||
class="displayText"
|
||||
>
|
||||
{{ suggestion.displayText }}
|
||||
</span>
|
||||
<span class="detailText">{{ suggestion.detailText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -116,11 +116,12 @@ export const suggestUsers = ({ dispatch, state }) => {
|
|||
|
||||
return diff + nameAlphabetically + screenNameAlphabetically
|
||||
/* eslint-disable camelcase */
|
||||
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
|
||||
displayText: screen_name_ui,
|
||||
detailText: name,
|
||||
imageUrl: profile_image_url_original,
|
||||
replacement: '@' + screen_name + ' '
|
||||
}).map((user) => ({
|
||||
user,
|
||||
displayText: user.screen_name_ui,
|
||||
detailText: user.name,
|
||||
imageUrl: user.profile_image_url_original,
|
||||
replacement: '@' + user.screen_name + ' '
|
||||
}))
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import ListsCard from '../lists_card/lists_card.vue'
|
||||
import ListsNew from '../lists_new/lists_new.vue'
|
||||
|
||||
const Lists = {
|
||||
data () {
|
||||
|
@ -8,11 +7,7 @@ const Lists = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
ListsCard,
|
||||
ListsNew
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('startFetchingLists')
|
||||
ListsCard
|
||||
},
|
||||
computed: {
|
||||
lists () {
|
||||
|
|
|
@ -1,21 +1,15 @@
|
|||
<template>
|
||||
<div v-if="isNew">
|
||||
<ListsNew @cancel="cancelNewList" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="settings panel panel-default"
|
||||
>
|
||||
<div class="Lists panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="title">
|
||||
{{ $t('lists.lists') }}
|
||||
</div>
|
||||
<button
|
||||
class="button-default"
|
||||
@click="newList"
|
||||
<router-link
|
||||
:to="{ name: 'lists-new' }"
|
||||
class="button-default btn new-list-button"
|
||||
>
|
||||
{{ $t("lists.new") }}
|
||||
</button>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ListsCard
|
||||
|
@ -29,3 +23,11 @@
|
|||
</template>
|
||||
|
||||
<script src="./lists.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.Lists {
|
||||
.new-list-button {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { mapState, mapGetters } from 'vuex'
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import ListsUserSearch from '../lists_user_search/lists_user_search.vue'
|
||||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faSearch,
|
||||
|
@ -17,22 +19,33 @@ const ListsNew = {
|
|||
components: {
|
||||
BasicUserCard,
|
||||
UserAvatar,
|
||||
ListsUserSearch
|
||||
ListsUserSearch,
|
||||
TabSwitcher,
|
||||
PanelLoading
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
title: '',
|
||||
userIds: [],
|
||||
selectedUserIds: []
|
||||
titleDraft: '',
|
||||
membersUserIds: [],
|
||||
removedUserIds: new Set([]), // users we added for members, to undo
|
||||
searchUserIds: [],
|
||||
addedUserIds: new Set([]), // users we added from search, to undo
|
||||
searchLoading: false,
|
||||
reallyDelete: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchList', { id: this.id })
|
||||
.then(() => { this.title = this.findListTitle(this.id) })
|
||||
this.$store.dispatch('fetchListAccounts', { id: this.id })
|
||||
if (!this.id) return
|
||||
this.$store.dispatch('fetchList', { listId: this.id })
|
||||
.then(() => {
|
||||
this.selectedUserIds = this.findListAccounts(this.id)
|
||||
this.selectedUserIds.forEach(userId => {
|
||||
this.title = this.findListTitle(this.id)
|
||||
this.titleDraft = this.title
|
||||
})
|
||||
this.$store.dispatch('fetchListAccounts', { listId: this.id })
|
||||
.then(() => {
|
||||
this.membersUserIds = this.findListAccounts(this.id)
|
||||
this.membersUserIds.forEach(userId => {
|
||||
this.$store.dispatch('fetchUserIfMissing', userId)
|
||||
})
|
||||
})
|
||||
|
@ -41,11 +54,12 @@ const ListsNew = {
|
|||
id () {
|
||||
return this.$route.params.id
|
||||
},
|
||||
users () {
|
||||
return this.userIds.map(userId => this.findUser(userId))
|
||||
membersUsers () {
|
||||
return [...this.membersUserIds, ...this.addedUserIds]
|
||||
.map(userId => this.findUser(userId)).filter(user => user)
|
||||
},
|
||||
selectedUsers () {
|
||||
return this.selectedUserIds.map(userId => this.findUser(userId)).filter(user => user)
|
||||
searchUsers () {
|
||||
return this.searchUserIds.map(userId => this.findUser(userId)).filter(user => user)
|
||||
},
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
|
@ -56,33 +70,73 @@ const ListsNew = {
|
|||
onInput () {
|
||||
this.search(this.query)
|
||||
},
|
||||
selectUser (user) {
|
||||
if (this.selectedUserIds.includes(user.id)) {
|
||||
this.removeUser(user.id)
|
||||
toggleRemoveMember (user) {
|
||||
if (this.removedUserIds.has(user.id)) {
|
||||
this.id && this.addUser(user)
|
||||
this.removedUserIds.delete(user.id)
|
||||
} else {
|
||||
this.addUser(user)
|
||||
this.id && this.removeUser(user.id)
|
||||
this.removedUserIds.add(user.id)
|
||||
}
|
||||
},
|
||||
isSelected (user) {
|
||||
return this.selectedUserIds.includes(user.id)
|
||||
toggleAddFromSearch (user) {
|
||||
if (this.addedUserIds.has(user.id)) {
|
||||
this.id && this.removeUser(user.id)
|
||||
this.addedUserIds.delete(user.id)
|
||||
} else {
|
||||
this.id && this.addUser(user)
|
||||
this.addedUserIds.add(user.id)
|
||||
}
|
||||
},
|
||||
isRemoved (user) {
|
||||
return this.removedUserIds.has(user.id)
|
||||
},
|
||||
isAdded (user) {
|
||||
return this.addedUserIds.has(user.id)
|
||||
},
|
||||
addUser (user) {
|
||||
this.selectedUserIds.push(user.id)
|
||||
this.$store.dispatch('addListAccount', { accountId: this.user.id, listId: this.id })
|
||||
},
|
||||
removeUser (userId) {
|
||||
this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId)
|
||||
this.$store.dispatch('removeListAccount', { accountId: this.user.id, listId: this.id })
|
||||
},
|
||||
onResults (results) {
|
||||
this.userIds = results
|
||||
onSearchLoading (results) {
|
||||
this.searchLoading = true
|
||||
},
|
||||
updateList () {
|
||||
this.$store.dispatch('setList', { id: this.id, title: this.title })
|
||||
this.$store.dispatch('setListAccounts', { id: this.id, accountIds: this.selectedUserIds })
|
||||
|
||||
this.$router.push({ name: 'lists-timeline', params: { id: this.id } })
|
||||
onSearchLoadingDone (results) {
|
||||
this.searchLoading = false
|
||||
},
|
||||
onSearchResults (results) {
|
||||
this.searchLoading = false
|
||||
this.searchUserIds = results
|
||||
},
|
||||
updateListTitle () {
|
||||
this.$store.dispatch('setList', { listId: this.id, title: this.titleDraft })
|
||||
.then(() => {
|
||||
this.title = this.findListTitle(this.id)
|
||||
})
|
||||
},
|
||||
createList () {
|
||||
this.$store.dispatch('createList', { title: this.titleDraft })
|
||||
.then((list) => {
|
||||
return this
|
||||
.$store
|
||||
.dispatch('setListAccounts', { listId: list.id, accountIds: [...this.addedUserIds] })
|
||||
.then(() => list.id)
|
||||
})
|
||||
.then((listId) => {
|
||||
this.$router.push({ name: 'lists-timeline', params: { id: listId } })
|
||||
})
|
||||
.catch((e) => {
|
||||
this.$store.dispatch('pushGlobalNotice', {
|
||||
messageKey: 'lists.error',
|
||||
messageArgs: [e.message],
|
||||
level: 'error'
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteList () {
|
||||
this.$store.dispatch('deleteList', { id: this.id })
|
||||
this.$store.dispatch('deleteList', { listId: this.id })
|
||||
this.$router.push({ name: 'lists' })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div class="panel-default panel list-edit">
|
||||
<div class="panel-default panel ListEdit">
|
||||
<div
|
||||
ref="header"
|
||||
class="panel-heading"
|
||||
class="panel-heading list-edit-heading"
|
||||
>
|
||||
<button
|
||||
class="button-unstyled go-back-button"
|
||||
|
@ -13,54 +13,151 @@
|
|||
icon="chevron-left"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
ref="title"
|
||||
v-model="title"
|
||||
:placeholder="$t('lists.title')"
|
||||
>
|
||||
</div>
|
||||
<div class="member-list">
|
||||
<div
|
||||
v-for="user in selectedUsers"
|
||||
:key="user.id"
|
||||
class="member"
|
||||
>
|
||||
<BasicUserCard
|
||||
:user="user"
|
||||
:class="isSelected(user) ? 'selected' : ''"
|
||||
@click.capture.prevent="selectUser(user)"
|
||||
<div class="title">
|
||||
<i18n-t
|
||||
v-if="id"
|
||||
keypath="lists.editing_list"
|
||||
>
|
||||
<template #listTitle>
|
||||
{{ title }}
|
||||
</template>
|
||||
</i18n-t>
|
||||
<i18n-t
|
||||
v-else
|
||||
keypath="lists.creating_list"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ListsUserSearch @results="onResults" />
|
||||
<div class="member-list">
|
||||
<div
|
||||
v-for="user in users"
|
||||
:key="user.id"
|
||||
class="member"
|
||||
>
|
||||
<BasicUserCard
|
||||
:user="user"
|
||||
:class="isSelected(user) ? 'selected' : ''"
|
||||
@click.capture.prevent="selectUser(user)"
|
||||
/>
|
||||
<div class="panel-body">
|
||||
<div class="input-wrap">
|
||||
<label for="list-edit-title">{{ $t('lists.title') }}</label>
|
||||
{{ ' ' }}
|
||||
<input
|
||||
id="list-edit-title"
|
||||
ref="title"
|
||||
v-model="titleDraft"
|
||||
>
|
||||
<button
|
||||
v-if="id"
|
||||
class="btn button-default follow-button"
|
||||
@click="updateListTitle"
|
||||
>
|
||||
{{ $t('lists.update_title') }}
|
||||
</button>
|
||||
</div>
|
||||
<tab-switcher
|
||||
class="list-member-management"
|
||||
:scrollable-tabs="true"
|
||||
>
|
||||
<div
|
||||
v-if="id || addedUserIds.size > 0"
|
||||
:label="$t('lists.manage_members')"
|
||||
class="members-list"
|
||||
>
|
||||
<div class="users-list">
|
||||
<div
|
||||
v-for="user in membersUsers"
|
||||
:key="user.id"
|
||||
class="member"
|
||||
>
|
||||
<BasicUserCard
|
||||
:user="user"
|
||||
>
|
||||
<button
|
||||
class="btn button-default follow-button"
|
||||
@click="toggleRemoveMember(user)"
|
||||
>
|
||||
{{ isRemoved(user) ? $t('general.undo') : $t('lists.remove_from_list') }}
|
||||
</button>
|
||||
</BasicUserCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="search-list"
|
||||
:label="$t('lists.add_members')"
|
||||
>
|
||||
<ListsUserSearch
|
||||
@results="onSearchResults"
|
||||
@loading="onSearchLoading"
|
||||
@loadingDone="onSearchLoadingDone"
|
||||
/>
|
||||
<div
|
||||
v-if="searchLoading"
|
||||
class="loading"
|
||||
>
|
||||
<PanelLoading />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="users-list"
|
||||
>
|
||||
<div
|
||||
v-for="user in searchUsers"
|
||||
:key="user.id"
|
||||
class="member"
|
||||
>
|
||||
<BasicUserCard
|
||||
:user="user"
|
||||
>
|
||||
<span
|
||||
v-if="membersUserIds.includes(user.id)"
|
||||
>
|
||||
{{ $t('lists.is_in_list') }}
|
||||
</span>
|
||||
<button
|
||||
v-if="!membersUserIds.includes(user.id)"
|
||||
class="btn button-default follow-button"
|
||||
@click="toggleAddFromSearch(user)"
|
||||
>
|
||||
{{ isAdded(user) ? $t('general.undo') : $t('lists.add_to_list') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn button-default follow-button"
|
||||
@click="toggleRemoveMember(user)"
|
||||
>
|
||||
{{ isRemoved(user) ? $t('general.undo') : $t('lists.remove_from_list') }}
|
||||
</button>
|
||||
</BasicUserCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span class="spacer" />
|
||||
<button
|
||||
v-if="!id"
|
||||
class="btn button-default footer-button"
|
||||
@click="createList"
|
||||
>
|
||||
{{ $t('lists.create') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="!reallyDelete"
|
||||
class="btn button-default footer-button"
|
||||
@click="reallyDelete = true"
|
||||
>
|
||||
{{ $t('lists.delete') }}
|
||||
</button>
|
||||
<template v-else>
|
||||
{{ $t('lists.really_delete') }}
|
||||
<button
|
||||
class="btn button-default footer-button"
|
||||
@click="deleteList"
|
||||
>
|
||||
{{ $t('general.yes') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default footer-button"
|
||||
@click="reallyDelete = false"
|
||||
>
|
||||
{{ $t('general.no') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<button
|
||||
:disabled="title && title.length === 0"
|
||||
class="btn button-default"
|
||||
@click="updateList"
|
||||
>
|
||||
{{ $t('lists.save') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="deleteList"
|
||||
>
|
||||
{{ $t('lists.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -69,28 +166,43 @@
|
|||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.list-edit {
|
||||
.input-wrap {
|
||||
display: flex;
|
||||
margin: 0.7em 0.5em 0.7em 0.5em;
|
||||
.ListEdit {
|
||||
--panel-body-padding: 0.5em;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.list-edit-heading {
|
||||
grid-template-columns: auto minmax(50%, 1fr);
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-member-management {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
|
||||
.member-list {
|
||||
.users-list {
|
||||
padding-bottom: 0.7rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.basic-user-card:hover,
|
||||
.basic-user-card.selected {
|
||||
cursor: pointer;
|
||||
background-color: var(--selectedPost, $fallback--lightBg);
|
||||
& .search-list,
|
||||
& .members-list {
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.go-back-button {
|
||||
|
@ -102,7 +214,15 @@
|
|||
}
|
||||
|
||||
.btn {
|
||||
margin: 0.5em;
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
grid-template-columns: minmax(10%, 1fr);
|
||||
|
||||
.footer-button {
|
||||
min-width: 9em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,28 +1,17 @@
|
|||
import { mapState } from 'vuex'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faHome
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||
import { getListEntries } from 'src/components/navigation/filter.js'
|
||||
|
||||
library.add(
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faHome
|
||||
)
|
||||
|
||||
const ListsMenuContent = {
|
||||
created () {
|
||||
this.$store.dispatch('startFetchingLists')
|
||||
export const ListsMenuContent = {
|
||||
props: [
|
||||
'showPin'
|
||||
],
|
||||
components: {
|
||||
NavigationEntry
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
lists: state => state.lists.allLists,
|
||||
lists: getListEntries,
|
||||
currentUser: state => state.users.currentUser,
|
||||
privateMode: state => state.instance.private,
|
||||
federating: state => state.instance.federating
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
<template>
|
||||
<ul>
|
||||
<li
|
||||
v-for="list in lists.slice().reverse()"
|
||||
:key="list.id"
|
||||
>
|
||||
<router-link
|
||||
class="menu-item"
|
||||
:to="{ name: 'lists-timeline', params: { id: list.id } }"
|
||||
>
|
||||
{{ list.title }}
|
||||
</router-link>
|
||||
</li>
|
||||
<NavigationEntry
|
||||
v-for="item in lists"
|
||||
:key="item.name"
|
||||
:show-pin="showPin"
|
||||
:item="item"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
import { mapState, mapGetters } from 'vuex'
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import ListsUserSearch from '../lists_user_search/lists_user_search.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faSearch,
|
||||
faChevronLeft
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faSearch,
|
||||
faChevronLeft
|
||||
)
|
||||
|
||||
const ListsNew = {
|
||||
components: {
|
||||
BasicUserCard,
|
||||
UserAvatar,
|
||||
ListsUserSearch
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
title: '',
|
||||
userIds: [],
|
||||
selectedUserIds: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
users () {
|
||||
return this.userIds.map(userId => this.findUser(userId))
|
||||
},
|
||||
selectedUsers () {
|
||||
return this.selectedUserIds.map(userId => this.findUser(userId))
|
||||
},
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
...mapGetters(['findUser'])
|
||||
},
|
||||
methods: {
|
||||
goBack () {
|
||||
this.$emit('cancel')
|
||||
},
|
||||
onInput () {
|
||||
this.search(this.query)
|
||||
},
|
||||
selectUser (user) {
|
||||
if (this.selectedUserIds.includes(user.id)) {
|
||||
this.removeUser(user.id)
|
||||
} else {
|
||||
this.addUser(user)
|
||||
}
|
||||
},
|
||||
isSelected (user) {
|
||||
return this.selectedUserIds.includes(user.id)
|
||||
},
|
||||
addUser (user) {
|
||||
this.selectedUserIds.push(user.id)
|
||||
},
|
||||
removeUser (userId) {
|
||||
this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId)
|
||||
},
|
||||
onResults (results) {
|
||||
this.userIds = results
|
||||
},
|
||||
createList () {
|
||||
// the API has two different endpoints for "creating a list with a name"
|
||||
// and "updating the accounts on the list".
|
||||
this.$store.dispatch('createList', { title: this.title })
|
||||
.then((list) => {
|
||||
this.$store.dispatch('setListAccounts', { id: list.id, accountIds: this.selectedUserIds })
|
||||
this.$router.push({ name: 'lists-timeline', params: { id: list.id } })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ListsNew
|
|
@ -1,95 +0,0 @@
|
|||
<template>
|
||||
<div class="panel-default panel list-new">
|
||||
<div
|
||||
ref="header"
|
||||
class="panel-heading"
|
||||
>
|
||||
<button
|
||||
class="button-unstyled go-back-button"
|
||||
@click="goBack"
|
||||
>
|
||||
<FAIcon
|
||||
size="lg"
|
||||
icon="chevron-left"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
ref="title"
|
||||
v-model="title"
|
||||
:placeholder="$t('lists.title')"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="member-list">
|
||||
<div
|
||||
v-for="user in selectedUsers"
|
||||
:key="user.id"
|
||||
class="member"
|
||||
>
|
||||
<BasicUserCard
|
||||
:user="user"
|
||||
:class="isSelected(user) ? 'selected' : ''"
|
||||
@click.capture.prevent="selectUser(user)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ListsUserSearch
|
||||
@results="onResults"
|
||||
/>
|
||||
<div
|
||||
v-for="user in users"
|
||||
:key="user.id"
|
||||
class="member"
|
||||
>
|
||||
<BasicUserCard
|
||||
:user="user"
|
||||
:class="isSelected(user) ? 'selected' : ''"
|
||||
@click.capture.prevent="selectUser(user)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
:disabled="title && title.length === 0"
|
||||
class="btn button-default"
|
||||
@click="createList"
|
||||
>
|
||||
{{ $t('lists.create') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./lists_new.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.list-new {
|
||||
.search-icon {
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
|
||||
.member-list {
|
||||
padding-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.basic-user-card:hover,
|
||||
.basic-user-card.selected {
|
||||
cursor: pointer;
|
||||
background-color: var(--selectedPost, $fallback--lightBg);
|
||||
}
|
||||
|
||||
.go-back-button {
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
height: 100%;
|
||||
align-self: start;
|
||||
width: var(--__panel-heading-height-inner);
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin: 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -17,14 +17,14 @@ const ListsTimeline = {
|
|||
this.listId = route.params.id
|
||||
this.$store.dispatch('stopFetchingTimeline', 'list')
|
||||
this.$store.commit('clearTimeline', { timeline: 'list' })
|
||||
this.$store.dispatch('fetchList', { id: this.listId })
|
||||
this.$store.dispatch('fetchList', { listId: this.listId })
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId })
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.listId = this.$route.params.id
|
||||
this.$store.dispatch('fetchList', { id: this.listId })
|
||||
this.$store.dispatch('fetchList', { listId: this.listId })
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId })
|
||||
},
|
||||
unmounted () {
|
||||
|
|
|
@ -15,6 +15,7 @@ const ListsUserSearch = {
|
|||
components: {
|
||||
Checkbox
|
||||
},
|
||||
emits: ['loading', 'loadingDone', 'results'],
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
|
@ -33,12 +34,16 @@ const ListsUserSearch = {
|
|||
}
|
||||
|
||||
this.loading = true
|
||||
this.$emit('loading')
|
||||
this.userIds = []
|
||||
this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts', following: this.followingOnly })
|
||||
.then(data => {
|
||||
this.loading = false
|
||||
this.$emit('results', data.accounts.map(a => a.id))
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
this.$emit('loadingDone')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="ListsUserSearch">
|
||||
<div class="input-wrap">
|
||||
<div class="input-search">
|
||||
<FAIcon
|
||||
|
@ -29,17 +29,19 @@
|
|||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.input-wrap {
|
||||
display: flex;
|
||||
margin: 0.7em 0.5em 0.7em 0.5em;
|
||||
.ListsUserSearch {
|
||||
.input-wrap {
|
||||
display: flex;
|
||||
margin: 0.7em 0.5em 0.7em 0.5em;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -2,6 +2,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
|
|||
import { mapGetters, mapState } from 'vuex'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
@ -16,6 +17,7 @@ const MentionLink = {
|
|||
name: 'MentionLink',
|
||||
components: {
|
||||
UserAvatar,
|
||||
UnicodeDomainIndicator,
|
||||
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
||||
},
|
||||
props: {
|
||||
|
|
|
@ -47,6 +47,9 @@
|
|||
class="serverName"
|
||||
:class="{ '-faded': shouldFadeDomain }"
|
||||
v-html="'@' + serverName"
|
||||
/><UnicodeDomainIndicator
|
||||
v-if="shouldShowFullUserName"
|
||||
:user="user"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
|
|
|
@ -2,6 +2,7 @@ import SideDrawer from '../side_drawer/side_drawer.vue'
|
|||
import Notifications from '../notifications/notifications.vue'
|
||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
@ -19,7 +20,8 @@ library.add(
|
|||
const MobileNav = {
|
||||
components: {
|
||||
SideDrawer,
|
||||
Notifications
|
||||
Notifications,
|
||||
NavigationPins
|
||||
},
|
||||
data: () => ({
|
||||
notificationsCloseGesture: undefined,
|
||||
|
@ -47,7 +49,10 @@ const MobileNav = {
|
|||
isChat () {
|
||||
return this.$route.name === 'chat'
|
||||
},
|
||||
...mapGetters(['unreadChatCount'])
|
||||
...mapGetters(['unreadChatCount']),
|
||||
chatsPinned () {
|
||||
return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleMobileSidebar () {
|
||||
|
|
|
@ -17,20 +17,12 @@
|
|||
icon="bars"
|
||||
/>
|
||||
<div
|
||||
v-if="unreadChatCount"
|
||||
v-if="unreadChatCount && !chatsPinned"
|
||||
class="alert-dot"
|
||||
/>
|
||||
</button>
|
||||
<router-link
|
||||
v-if="!hideSitename"
|
||||
class="site-name"
|
||||
:to="{ name: 'root' }"
|
||||
active-class="home"
|
||||
>
|
||||
{{ sitename }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="item right">
|
||||
<NavigationPins class="pins" />
|
||||
</div> <div class="item right">
|
||||
<button
|
||||
v-if="currentUser"
|
||||
class="button-unstyled mobile-nav-button"
|
||||
|
@ -94,6 +86,7 @@
|
|||
grid-template-columns: 2fr auto;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
a {
|
||||
color: var(--topBarLink, $fallback--link);
|
||||
}
|
||||
|
@ -178,13 +171,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
.pins {
|
||||
flex: 1;
|
||||
|
||||
.pinned-item {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-notifications {
|
||||
margin-top: 50px;
|
||||
width: 100vw;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
background-color: $fallback--bg;
|
||||
|
@ -194,14 +194,17 @@
|
|||
padding: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
|
||||
.panel {
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.panel:after {
|
||||
|
||||
.panel::after {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.panel .panel-heading {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
|
|
|
@ -10,7 +10,8 @@ library.add(
|
|||
|
||||
const HIDDEN_FOR_PAGES = new Set([
|
||||
'chats',
|
||||
'chat'
|
||||
'chat',
|
||||
'lists-edit'
|
||||
])
|
||||
|
||||
const MobilePostStatusButton = {
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import TimelineMenuContent from '../timeline_menu/timeline_menu_content.vue'
|
||||
import ListsMenuContent from '../lists_menu/lists_menu_content.vue'
|
||||
import ListsMenuContent from 'src/components/lists_menu/lists_menu_content.vue'
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import { TIMELINES, ROOT_ITEMS } from 'src/components/navigation/navigation.js'
|
||||
import { filterNavigation } from 'src/components/navigation/filter.js'
|
||||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
@ -30,21 +34,23 @@ library.add(
|
|||
faStream,
|
||||
faList
|
||||
)
|
||||
|
||||
const NavPanel = {
|
||||
props: ['forceExpand', 'forceEditMode'],
|
||||
created () {
|
||||
if (this.currentUser && this.currentUser.locked) {
|
||||
this.$store.dispatch('startFetchingFollowRequests')
|
||||
}
|
||||
},
|
||||
components: {
|
||||
TimelineMenuContent,
|
||||
ListsMenuContent
|
||||
ListsMenuContent,
|
||||
NavigationEntry,
|
||||
NavigationPins,
|
||||
Checkbox
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
editMode: false,
|
||||
showTimelines: false,
|
||||
showLists: false
|
||||
showLists: false,
|
||||
timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k })),
|
||||
rootList: Object.entries(ROOT_ITEMS).map(([k, v]) => ({ ...v, name: k }))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -53,19 +59,62 @@ const NavPanel = {
|
|||
},
|
||||
toggleLists () {
|
||||
this.showLists = !this.showLists
|
||||
},
|
||||
toggleEditMode () {
|
||||
this.editMode = !this.editMode
|
||||
},
|
||||
toggleCollapse () {
|
||||
this.$store.commit('setPreference', { path: 'simple.collapseNav', value: !this.collapsed })
|
||||
this.$store.dispatch('pushServerSideStorage')
|
||||
},
|
||||
isPinned (item) {
|
||||
return this.pinnedItems.has(item)
|
||||
},
|
||||
togglePin (item) {
|
||||
if (this.isPinned(item)) {
|
||||
this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value: item })
|
||||
} else {
|
||||
this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value: item })
|
||||
}
|
||||
this.$store.dispatch('pushServerSideStorage')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
listsNavigation () {
|
||||
return this.$store.getters.mergedConfig.listsNavigation
|
||||
},
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
followRequestCount: state => state.api.followRequests.length,
|
||||
privateMode: state => state.instance.private,
|
||||
federating: state => state.instance.federating,
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
||||
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems),
|
||||
collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav
|
||||
}),
|
||||
timelinesItems () {
|
||||
return filterNavigation(
|
||||
Object
|
||||
.entries({ ...TIMELINES })
|
||||
.map(([k, v]) => ({ ...v, name: k })),
|
||||
{
|
||||
hasChats: this.pleromaChatMessagesAvailable,
|
||||
isFederating: this.federating,
|
||||
isPrivate: this.privateMode,
|
||||
currentUser: this.currentUser
|
||||
}
|
||||
)
|
||||
},
|
||||
rootItems () {
|
||||
return filterNavigation(
|
||||
Object
|
||||
.entries({ ...ROOT_ITEMS })
|
||||
.map(([k, v]) => ({ ...v, name: k })),
|
||||
{
|
||||
hasChats: this.pleromaChatMessagesAvailable,
|
||||
isFederating: this.federating,
|
||||
isPrivate: this.privateMode,
|
||||
currentUser: this.currentUser
|
||||
}
|
||||
)
|
||||
},
|
||||
...mapGetters(['unreadChatCount'])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,135 +1,99 @@
|
|||
<template>
|
||||
<div class="NavPanel">
|
||||
<div class="panel panel-default">
|
||||
<ul>
|
||||
<li v-if="currentUser || !privateMode">
|
||||
<button
|
||||
class="button-unstyled menu-item"
|
||||
@click="toggleTimelines"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110"
|
||||
icon="stream"
|
||||
/>{{ $t("nav.timelines") }}
|
||||
<FAIcon
|
||||
class="timelines-chevron"
|
||||
fixed-width
|
||||
:icon="showTimelines ? 'chevron-up' : 'chevron-down'"
|
||||
<div
|
||||
v-if="!forceExpand"
|
||||
class="panel-heading nav-panel-heading"
|
||||
>
|
||||
<NavigationPins :limit="6" />
|
||||
<div class="spacer" />
|
||||
<button
|
||||
class="button-unstyled"
|
||||
@click="toggleCollapse"
|
||||
>
|
||||
<FAIcon
|
||||
class="timelines-chevron"
|
||||
fixed-width
|
||||
:icon="collapsed ? 'chevron-down' : 'chevron-up'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<ul
|
||||
v-if="!collapsed || forceExpand"
|
||||
class="panel-body"
|
||||
>
|
||||
<NavigationEntry
|
||||
v-if="currentUser || !privateMode"
|
||||
:show-pin="false"
|
||||
:item="{ icon: 'stream', label: 'nav.timelines' }"
|
||||
:aria-expanded="showTimelines ? 'true' : 'false'"
|
||||
@click="toggleTimelines"
|
||||
>
|
||||
<FAIcon
|
||||
class="timelines-chevron"
|
||||
fixed-width
|
||||
:icon="showTimelines ? 'chevron-up' : 'chevron-down'"
|
||||
/>
|
||||
</NavigationEntry>
|
||||
<div
|
||||
v-show="showTimelines"
|
||||
class="timelines-background"
|
||||
>
|
||||
<div class="timelines">
|
||||
<NavigationEntry
|
||||
v-for="item in timelinesItems"
|
||||
:key="item.name"
|
||||
:show-pin="editMode || forceEditMode"
|
||||
:item="item"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
v-show="showTimelines"
|
||||
class="timelines-background"
|
||||
>
|
||||
<TimelineMenuContent class="timelines" />
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="currentUser && listsNavigation">
|
||||
<button
|
||||
class="button-unstyled menu-item"
|
||||
@click="toggleLists"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'lists' }"
|
||||
@click.stop
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110"
|
||||
icon="list"
|
||||
/>{{ $t("nav.lists") }}
|
||||
</router-link>
|
||||
<FAIcon
|
||||
class="timelines-chevron"
|
||||
fixed-width
|
||||
:icon="showLists ? 'chevron-up' : 'chevron-down'"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
v-show="showLists"
|
||||
class="timelines-background"
|
||||
>
|
||||
<ListsMenuContent class="timelines" />
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="currentUser && !listsNavigation">
|
||||
</div>
|
||||
<NavigationEntry
|
||||
v-if="currentUser"
|
||||
:show-pin="false"
|
||||
:item="{ icon: 'list', label: 'nav.lists' }"
|
||||
:aria-expanded="showLists ? 'true' : 'false'"
|
||||
@click="toggleLists"
|
||||
>
|
||||
<router-link
|
||||
:title="$t('lists.manage_lists')"
|
||||
class="extra-button"
|
||||
:to="{ name: 'lists' }"
|
||||
@click.stop
|
||||
>
|
||||
<button
|
||||
class="button-unstyled menu-item"
|
||||
@click="toggleLists"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110"
|
||||
icon="list"
|
||||
/>{{ $t("nav.lists") }}
|
||||
</button>
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="currentUser">
|
||||
<router-link
|
||||
class="menu-item"
|
||||
:to="{ name: 'interactions', params: { username: currentUser.screen_name } }"
|
||||
>
|
||||
<FAIcon
|
||||
class="extra-button"
|
||||
fixed-width
|
||||
class="fa-scale-110"
|
||||
icon="bell"
|
||||
/>{{ $t("nav.interactions") }}
|
||||
icon="wrench"
|
||||
/>
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="currentUser && pleromaChatMessagesAvailable">
|
||||
<router-link
|
||||
class="menu-item"
|
||||
:to="{ name: 'chats', params: { username: currentUser.screen_name } }"
|
||||
>
|
||||
<div
|
||||
v-if="unreadChatCount"
|
||||
class="badge badge-notification"
|
||||
>
|
||||
{{ unreadChatCount }}
|
||||
</div>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110"
|
||||
icon="comments"
|
||||
/>{{ $t("nav.chats") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="currentUser && currentUser.locked">
|
||||
<router-link
|
||||
class="menu-item"
|
||||
:to="{ name: 'friend-requests' }"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110"
|
||||
icon="user-plus"
|
||||
/>{{ $t("nav.friend_requests") }}
|
||||
<span
|
||||
v-if="followRequestCount > 0"
|
||||
class="badge badge-notification"
|
||||
>
|
||||
{{ followRequestCount }}
|
||||
</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
class="menu-item"
|
||||
:to="{ name: 'about' }"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110"
|
||||
icon="info-circle"
|
||||
/>{{ $t("nav.about") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<FAIcon
|
||||
class="timelines-chevron"
|
||||
fixed-width
|
||||
:icon="showLists ? 'chevron-up' : 'chevron-down'"
|
||||
/>
|
||||
</NavigationEntry>
|
||||
<div
|
||||
v-show="showLists"
|
||||
class="timelines-background"
|
||||
>
|
||||
<ListsMenuContent
|
||||
:show-pin="editMode || forceEditMode"
|
||||
class="timelines"
|
||||
/>
|
||||
</div>
|
||||
<NavigationEntry
|
||||
v-for="item in rootItems"
|
||||
:key="item.name"
|
||||
:show-pin="editMode || forceEditMode"
|
||||
:item="item"
|
||||
/>
|
||||
<NavigationEntry
|
||||
v-if="!forceEditMode && currentUser"
|
||||
:show-pin="false"
|
||||
:item="{ label: editMode ? $t('nav.edit_finish') : $t('nav.edit_pinned'), icon: editMode ? 'check' : 'wrench' }"
|
||||
@click="toggleEditMode"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -180,54 +144,23 @@
|
|||
border: none;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
height: 3.5em;
|
||||
line-height: 3.5em;
|
||||
padding: 0 1em;
|
||||
width: 100%;
|
||||
color: $fallback--link;
|
||||
color: var(--link, $fallback--link);
|
||||
|
||||
&:hover {
|
||||
background-color: $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(--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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timelines-chevron {
|
||||
margin-left: 0.8em;
|
||||
margin-right: 0.8em;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
.timelines-chevron {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.timelines-background {
|
||||
padding: 0 0 0 0.6em;
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||
border-top: 1px solid;
|
||||
border-bottom: 1px solid;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
}
|
||||
|
@ -237,14 +170,9 @@
|
|||
background-color: var(--bg, $fallback--bg);
|
||||
}
|
||||
|
||||
.fa-scale-110 {
|
||||
margin-right: 0.8em;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
right: 0.6rem;
|
||||
top: 1.25em;
|
||||
.nav-panel-heading {
|
||||
// breaks without a unit
|
||||
--panel-heading-height-padding: 0em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
18
src/components/navigation/filter.js
Normal file
18
src/components/navigation/filter.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
export const filterNavigation = (list = [], { hasChats, isFederating, isPrivate, currentUser }) => {
|
||||
return list.filter(({ criteria, anon, anonRoute }) => {
|
||||
const set = new Set(criteria || [])
|
||||
if (!isFederating && set.has('federating')) return false
|
||||
if (isPrivate && set.has('!private')) return false
|
||||
if (!currentUser && !(anon || anonRoute)) return false
|
||||
if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) return false
|
||||
if (!hasChats && set.has('chats')) return false
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export const getListEntries = state => state.lists.allLists.map(list => ({
|
||||
name: 'list-' + list.id,
|
||||
routeObject: { name: 'lists-timeline', params: { id: list.id } },
|
||||
labelRaw: list.title,
|
||||
iconLetter: list.title[0]
|
||||
}))
|
75
src/components/navigation/navigation.js
Normal file
75
src/components/navigation/navigation.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
export const USERNAME_ROUTES = new Set([
|
||||
'bookmarks',
|
||||
'dms',
|
||||
'interactions',
|
||||
'notifications',
|
||||
'chat',
|
||||
'chats',
|
||||
'user-profile'
|
||||
])
|
||||
|
||||
export const TIMELINES = {
|
||||
home: {
|
||||
route: 'friends',
|
||||
icon: 'home',
|
||||
label: 'nav.home_timeline',
|
||||
criteria: ['!private']
|
||||
},
|
||||
public: {
|
||||
route: 'public-timeline',
|
||||
anon: true,
|
||||
icon: 'users',
|
||||
label: 'nav.public_tl',
|
||||
criteria: ['!private']
|
||||
},
|
||||
twkn: {
|
||||
route: 'public-external-timeline',
|
||||
anon: true,
|
||||
icon: 'globe',
|
||||
label: 'nav.twkn',
|
||||
criteria: ['!private', 'federating']
|
||||
},
|
||||
bookmarks: {
|
||||
route: 'bookmarks',
|
||||
icon: 'bookmark',
|
||||
label: 'nav.bookmarks'
|
||||
},
|
||||
favorites: {
|
||||
routeObject: { name: 'user-profile', query: { tab: 'favorites' } },
|
||||
icon: 'star',
|
||||
label: 'user_card.favorites'
|
||||
},
|
||||
dms: {
|
||||
route: 'dms',
|
||||
icon: 'envelope',
|
||||
label: 'nav.dms'
|
||||
}
|
||||
}
|
||||
|
||||
export const ROOT_ITEMS = {
|
||||
interactions: {
|
||||
route: 'interactions',
|
||||
icon: 'bell',
|
||||
label: 'nav.interactions'
|
||||
},
|
||||
chats: {
|
||||
route: 'chats',
|
||||
icon: 'comments',
|
||||
label: 'nav.chats',
|
||||
badgeGetter: 'unreadChatCount',
|
||||
criteria: ['chats']
|
||||
},
|
||||
friendRequests: {
|
||||
route: 'friend-requests',
|
||||
icon: 'user-plus',
|
||||
label: 'nav.friend_requests',
|
||||
criteria: ['lockedUser'],
|
||||
badgeGetter: 'followRequestCount'
|
||||
},
|
||||
about: {
|
||||
route: 'about',
|
||||
anon: true,
|
||||
icon: 'info-circle',
|
||||
label: 'nav.about'
|
||||
}
|
||||
}
|
47
src/components/navigation/navigation_entry.js
Normal file
47
src/components/navigation/navigation_entry.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { mapState } from 'vuex'
|
||||
import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faThumbtack } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faThumbtack)
|
||||
|
||||
const NavigationEntry = {
|
||||
props: ['item', 'showPin'],
|
||||
methods: {
|
||||
isPinned (value) {
|
||||
return this.pinnedItems.has(value)
|
||||
},
|
||||
togglePin (value) {
|
||||
if (this.isPinned(value)) {
|
||||
this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value })
|
||||
} else {
|
||||
this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value })
|
||||
}
|
||||
this.$store.dispatch('pushServerSideStorage')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
routeTo () {
|
||||
if (!this.item.route && !this.item.routeObject) return null
|
||||
let route
|
||||
if (this.item.routeObject) {
|
||||
route = this.item.routeObject
|
||||
} else {
|
||||
route = { name: (this.item.anon || this.currentUser) ? this.item.route : this.item.anonRoute }
|
||||
}
|
||||
if (USERNAME_ROUTES.has(route.name)) {
|
||||
route.params = { username: this.currentUser.screen_name, name: this.currentUser.screen_name }
|
||||
}
|
||||
return route
|
||||
},
|
||||
getters () {
|
||||
return this.$store.getters
|
||||
},
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default NavigationEntry
|
120
src/components/navigation/navigation_entry.vue
Normal file
120
src/components/navigation/navigation_entry.vue
Normal file
|
@ -0,0 +1,120 @@
|
|||
<template>
|
||||
<li class="NavigationEntry">
|
||||
<component
|
||||
:is="routeTo ? 'router-link' : 'button'"
|
||||
class="menu-item button-unstyled"
|
||||
:to="routeTo"
|
||||
>
|
||||
<span>
|
||||
<FAIcon
|
||||
v-if="item.icon"
|
||||
fixed-width
|
||||
class="fa-scale-110 menu-icon"
|
||||
:icon="item.icon"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
v-if="item.iconLetter"
|
||||
class="icon iconLetter fa-scale-110 menu-icon"
|
||||
>{{ item.iconLetter }}
|
||||
</span>
|
||||
<span class="label">
|
||||
{{ item.labelRaw || $t(item.label) }}
|
||||
</span>
|
||||
<slot />
|
||||
<div
|
||||
v-if="item.badgeGetter && getters[item.badgeGetter]"
|
||||
class="badge badge-notification"
|
||||
>
|
||||
{{ getters[item.badgeGetter] }}
|
||||
</div>
|
||||
<button
|
||||
v-if="showPin && currentUser"
|
||||
type="button"
|
||||
class="button-unstyled extra-button"
|
||||
:title="$t(isPinned ? 'general.unpin' : 'general.pin' )"
|
||||
:aria-pressed="!!isPinned"
|
||||
@click.stop.prevent="togglePin(item.name)"
|
||||
>
|
||||
<FAIcon
|
||||
v-if="showPin && currentUser"
|
||||
fixed-width
|
||||
class="fa-scale-110"
|
||||
:class="{ 'veryfaint': !isPinned(item.name) }"
|
||||
:transform="!isPinned(item.name) ? 'rotate-45' : ''"
|
||||
icon="thumbtack"
|
||||
/>
|
||||
</button>
|
||||
</component>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script src="./navigation_entry.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.NavigationEntry {
|
||||
.label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
margin-right: 0.8em;
|
||||
}
|
||||
|
||||
.extra-button {
|
||||
width: 3em;
|
||||
text-align: center;
|
||||
|
||||
&:last-child {
|
||||
margin-right: -0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
align-items: baseline;
|
||||
height: 3.5em;
|
||||
line-height: 3.5em;
|
||||
padding: 0 1em;
|
||||
width: 100%;
|
||||
color: $fallback--link;
|
||||
color: var(--link, $fallback--link);
|
||||
|
||||
&:hover {
|
||||
background-color: $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);
|
||||
|
||||
.menu-icon {
|
||||
--icon: var(--text, $fallback--icon);
|
||||
}
|
||||
}
|
||||
|
||||
&.router-link-active {
|
||||
font-weight: bolder;
|
||||
background-color: $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);
|
||||
|
||||
.menu-icon {
|
||||
--icon: var(--text, $fallback--icon);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
88
src/components/navigation/navigation_pins.js
Normal file
88
src/components/navigation/navigation_pins.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
import { mapState } from 'vuex'
|
||||
import { TIMELINES, ROOT_ITEMS, USERNAME_ROUTES } from 'src/components/navigation/navigation.js'
|
||||
import { getListEntries, filterNavigation } from 'src/components/navigation/filter.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faComments,
|
||||
faBell,
|
||||
faInfoCircle,
|
||||
faStream,
|
||||
faList
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faComments,
|
||||
faBell,
|
||||
faInfoCircle,
|
||||
faStream,
|
||||
faList
|
||||
)
|
||||
|
||||
const NavPanel = {
|
||||
props: ['limit'],
|
||||
methods: {
|
||||
getRouteTo (item) {
|
||||
if (item.routeObject) {
|
||||
return item.routeObject
|
||||
}
|
||||
const route = { name: (item.anon || this.currentUser) ? item.route : item.anonRoute }
|
||||
if (USERNAME_ROUTES.has(route.name)) {
|
||||
route.params = { username: this.currentUser.screen_name }
|
||||
}
|
||||
return route
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getters () {
|
||||
return this.$store.getters
|
||||
},
|
||||
...mapState({
|
||||
lists: getListEntries,
|
||||
currentUser: state => state.users.currentUser,
|
||||
followRequestCount: state => state.api.followRequests.length,
|
||||
privateMode: state => state.instance.private,
|
||||
federating: state => state.instance.federating,
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
||||
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems)
|
||||
}),
|
||||
pinnedList () {
|
||||
if (!this.currentUser) {
|
||||
return [
|
||||
{ ...TIMELINES.public, name: 'public' },
|
||||
{ ...TIMELINES.twkn, name: 'twkn' },
|
||||
{ ...ROOT_ITEMS.about, name: 'about' }
|
||||
]
|
||||
}
|
||||
return filterNavigation(
|
||||
[
|
||||
...Object
|
||||
.entries({ ...TIMELINES })
|
||||
.filter(([k]) => this.pinnedItems.has(k))
|
||||
.map(([k, v]) => ({ ...v, name: k })),
|
||||
...this.lists.filter((k) => this.pinnedItems.has(k.name)),
|
||||
...Object
|
||||
.entries({ ...ROOT_ITEMS })
|
||||
.filter(([k]) => this.pinnedItems.has(k))
|
||||
.map(([k, v]) => ({ ...v, name: k }))
|
||||
],
|
||||
{
|
||||
hasChats: this.pleromaChatMessagesAvailable,
|
||||
isFederating: this.federating,
|
||||
isPrivate: this.privateMode,
|
||||
currentUser: this.currentUser
|
||||
}
|
||||
).slice(0, this.limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NavPanel
|
76
src/components/navigation/navigation_pins.vue
Normal file
76
src/components/navigation/navigation_pins.vue
Normal file
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<span class="NavigationPins">
|
||||
<router-link
|
||||
v-for="item in pinnedList"
|
||||
:key="item.name"
|
||||
class="pinned-item"
|
||||
:to="getRouteTo(item)"
|
||||
:title="item.labelRaw || $t(item.label)"
|
||||
>
|
||||
<FAIcon
|
||||
v-if="item.icon"
|
||||
fixed-width
|
||||
:icon="item.icon"
|
||||
/>
|
||||
<span
|
||||
v-if="item.iconLetter"
|
||||
class="iconLetter fa-scale-110 fa-old-padding"
|
||||
>{{ item.iconLetter }}</span>
|
||||
<div
|
||||
v-if="item.badgeGetter && getters[item.badgeGetter]"
|
||||
class="alert-dot"
|
||||
/>
|
||||
</router-link>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./navigation_pins.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
.NavigationPins {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
.alert-dot {
|
||||
border-radius: 100%;
|
||||
height: 0.5em;
|
||||
width: 0.5em;
|
||||
position: absolute;
|
||||
right: calc(50% - 0.25em);
|
||||
top: calc(50% - 0.25em);
|
||||
margin-left: 6px;
|
||||
margin-top: -6px;
|
||||
background-color: $fallback--cRed;
|
||||
background-color: var(--badgeNotification, $fallback--cRed);
|
||||
}
|
||||
|
||||
.pinned-item {
|
||||
position: relative;
|
||||
flex: 1 0 3em;
|
||||
min-width: 2em;
|
||||
text-align: center;
|
||||
overflow: visible;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
|
||||
& .svg-inline--fa,
|
||||
& .iconLetter {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.router-link-active {
|
||||
color: $fallback--text;
|
||||
color: var(--selectedMenuText, $fallback--text);
|
||||
border-bottom: 4px solid;
|
||||
|
||||
& .svg-inline--fa,
|
||||
& .iconLetter {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -5,6 +5,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue'
|
|||
import UserCard from '../user_card/user_card.vue'
|
||||
import Timeago from '../timeago/timeago.vue'
|
||||
import Report from '../report/report.vue'
|
||||
import UserLink from '../user_link/user_link.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import UserPopover from '../user_popover/user_popover.vue'
|
||||
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
|
||||
|
@ -50,7 +51,8 @@ const Notification = {
|
|||
Status,
|
||||
Report,
|
||||
RichContent,
|
||||
UserPopover
|
||||
UserPopover,
|
||||
UserLink
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
|
|
|
@ -11,9 +11,10 @@
|
|||
class="Notification container -muted"
|
||||
>
|
||||
<small>
|
||||
<router-link :to="userProfileLink">
|
||||
{{ notification.from_profile.screen_name_ui }}
|
||||
</router-link>
|
||||
<user-link
|
||||
:user="notification.from_profile"
|
||||
:at="false"
|
||||
/>
|
||||
</small>
|
||||
<button
|
||||
class="button-unstyled unmute"
|
||||
|
@ -174,12 +175,10 @@
|
|||
v-if="notification.type === 'follow' || notification.type === 'follow_request'"
|
||||
class="follow-text"
|
||||
>
|
||||
<router-link
|
||||
:to="userProfileLink"
|
||||
<user-link
|
||||
class="follow-name"
|
||||
>
|
||||
@{{ notification.from_profile.screen_name_ui }}
|
||||
</router-link>
|
||||
:user="notification.from_profile"
|
||||
/>
|
||||
<div
|
||||
v-if="notification.type === 'follow_request'"
|
||||
style="white-space: nowrap;"
|
||||
|
@ -210,9 +209,9 @@
|
|||
v-else-if="notification.type === 'move'"
|
||||
class="move-text"
|
||||
>
|
||||
<router-link :to="targetUserProfileLink">
|
||||
@{{ notification.target.screen_name_ui }}
|
||||
</router-link>
|
||||
<user-link
|
||||
:user="notification.target"
|
||||
/>
|
||||
</div>
|
||||
<Report
|
||||
v-else-if="notification.type === 'pleroma:report'"
|
||||
|
|
|
@ -4,7 +4,7 @@ const Popover = {
|
|||
// Action to trigger popover: either 'hover' or 'click'
|
||||
trigger: String,
|
||||
|
||||
// Either 'top' or 'bottom'
|
||||
// 'top', 'bottom', 'left', 'right'
|
||||
placement: String,
|
||||
|
||||
// Takes object with properties 'x' and 'y', values of these can be
|
||||
|
@ -84,6 +84,8 @@ const Popover = {
|
|||
const anchorStyle = getComputedStyle(anchorEl)
|
||||
const topPadding = parseFloat(anchorStyle.paddingTop)
|
||||
const bottomPadding = parseFloat(anchorStyle.paddingBottom)
|
||||
const rightPadding = parseFloat(anchorStyle.paddingRight)
|
||||
const leftPadding = parseFloat(anchorStyle.paddingLeft)
|
||||
|
||||
// Screen position of the origin point for popover = center of the anchor
|
||||
const origin = {
|
||||
|
@ -170,7 +172,7 @@ const Popover = {
|
|||
if (overlayCenter) {
|
||||
translateX = origin.x + horizOffset
|
||||
translateY = origin.y + vertOffset
|
||||
} else {
|
||||
} else if (this.placement !== 'right' && this.placement !== 'left') {
|
||||
// Default to whatever user wished with placement prop
|
||||
let usingTop = this.placement !== 'bottom'
|
||||
|
||||
|
@ -189,6 +191,25 @@ const Popover = {
|
|||
|
||||
const xOffset = (this.offset && this.offset.x) || 0
|
||||
translateX = origin.x + horizOffset + xOffset
|
||||
} else {
|
||||
// Default to whatever user wished with placement prop
|
||||
let usingRight = this.placement !== 'left'
|
||||
|
||||
// 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.
|
||||
const rightBoundary = origin.x - anchorWidth * 0.5 + (this.removePadding ? rightPadding : 0)
|
||||
const leftBoundary = origin.x + anchorWidth * 0.5 - (this.removePadding ? leftPadding : 0)
|
||||
if (leftBoundary + content.offsetWidth > xBounds.max) usingRight = true
|
||||
if (rightBoundary - content.offsetWidth < xBounds.min) usingRight = false
|
||||
|
||||
const xOffset = (this.offset && this.offset.x) || 0
|
||||
translateX = usingRight
|
||||
? rightBoundary - xOffset - content.offsetWidth
|
||||
: leftBoundary + xOffset
|
||||
|
||||
const yOffset = (this.offset && this.offset.y) || 0
|
||||
translateY = origin.y + vertOffset + yOffset
|
||||
}
|
||||
|
||||
this.styles = {
|
||||
|
|
|
@ -126,6 +126,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.-has-submenu {
|
||||
.chevron-icon {
|
||||
margin-right: 0.25rem;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:active, &:hover {
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--selectedMenuPopover, $fallback--lightBg);
|
||||
|
|
|
@ -47,6 +47,8 @@
|
|||
class="cancel-icon fa-scale-110 fa-old-padding"
|
||||
/>
|
||||
</button>
|
||||
<span class="spacer" />
|
||||
<span class="spacer" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -101,11 +101,6 @@
|
|||
{{ $t('settings.hide_shoutbox') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="listsNavigation">
|
||||
{{ $t('settings.lists_navigation') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<h3>{{ $t('settings.columns') }}</h3>
|
||||
</li>
|
||||
|
|
|
@ -2,6 +2,7 @@ import { mapState, mapGetters } from 'vuex'
|
|||
import UserCard from '../user_card/user_card.vue'
|
||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faSignInAlt,
|
||||
|
@ -15,6 +16,7 @@ import {
|
|||
faTachometerAlt,
|
||||
faCog,
|
||||
faInfoCircle,
|
||||
faCompass,
|
||||
faList
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
|
@ -30,6 +32,7 @@ library.add(
|
|||
faTachometerAlt,
|
||||
faCog,
|
||||
faInfoCircle,
|
||||
faCompass,
|
||||
faList
|
||||
)
|
||||
|
||||
|
@ -80,10 +83,16 @@ const SideDrawer = {
|
|||
return this.$store.state.instance.federating
|
||||
},
|
||||
timelinesRoute () {
|
||||
let name
|
||||
if (this.$store.state.interface.lastTimeline) {
|
||||
return this.$store.state.interface.lastTimeline
|
||||
name = this.$store.state.interface.lastTimeline
|
||||
}
|
||||
name = this.currentUser ? 'friends' : 'public-timeline'
|
||||
if (USERNAME_ROUTES.has(name)) {
|
||||
return { name, params: { username: this.currentUser.screen_name } }
|
||||
} else {
|
||||
return { name }
|
||||
}
|
||||
return this.currentUser ? 'friends' : 'public-timeline'
|
||||
},
|
||||
...mapState({
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
v-if="currentUser || !privateMode"
|
||||
@click="toggleDrawer"
|
||||
>
|
||||
<router-link :to="{ name: timelinesRoute }">
|
||||
<router-link :to="timelinesRoute">
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
|
@ -191,6 +191,18 @@
|
|||
/> {{ $t("nav.administration") }}
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="currentUser"
|
||||
@click="toggleDrawer"
|
||||
>
|
||||
<router-link :to="{ name: 'edit-navigation' }">
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="compass"
|
||||
/> {{ $t("nav.edit_nav_mobile") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li
|
||||
v-if="currentUser"
|
||||
@click="toggleDrawer"
|
||||
|
|
|
@ -13,6 +13,7 @@ import StatusPopover from '../status_popover/status_popover.vue'
|
|||
import UserPopover from '../user_popover/user_popover.vue'
|
||||
import UserListPopover from '../user_list_popover/user_list_popover.vue'
|
||||
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
|
||||
import UserLink from '../user_link/user_link.vue'
|
||||
import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
|
||||
import MentionLink from 'src/components/mention_link/mention_link.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
@ -115,7 +116,8 @@ const Status = {
|
|||
RichContent,
|
||||
MentionLink,
|
||||
MentionsLine,
|
||||
UserPopover
|
||||
UserPopover,
|
||||
UserLink
|
||||
},
|
||||
props: [
|
||||
'statusoid',
|
||||
|
|
|
@ -25,9 +25,10 @@
|
|||
class="fa-scale-110 fa-old-padding repeat-icon"
|
||||
icon="retweet"
|
||||
/>
|
||||
<router-link :to="userProfileLink">
|
||||
{{ status.user.screen_name_ui }}
|
||||
</router-link>
|
||||
<user-link
|
||||
:user="status.user"
|
||||
:at="false"
|
||||
/>
|
||||
</small>
|
||||
<small
|
||||
v-if="showReasonMutedThread"
|
||||
|
@ -164,13 +165,12 @@
|
|||
>
|
||||
{{ status.user.name }}
|
||||
</h4>
|
||||
<router-link
|
||||
<user-link
|
||||
class="account-name"
|
||||
:title="status.user.screen_name_ui"
|
||||
:to="userProfileLink"
|
||||
>
|
||||
{{ status.user.screen_name_ui }}
|
||||
</router-link>
|
||||
:user="status.user"
|
||||
:at="false"
|
||||
/>
|
||||
<img
|
||||
v-if="!!(status.user && status.user.favicon)"
|
||||
class="status-favicon"
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
overflow-x: auto;
|
||||
padding-top: 5px;
|
||||
flex-direction: row;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&::after, &::before {
|
||||
content: '';
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<template>
|
||||
<div :class="['Timeline', classes.root]">
|
||||
<div :class="classes.header">
|
||||
<TimelineMenu v-if="!embedded" />
|
||||
<TimelineMenu
|
||||
v-if="!embedded"
|
||||
:timeline-name="timelineName"
|
||||
/>
|
||||
<button
|
||||
v-if="showScrollTop"
|
||||
class="button-unstyled scroll-to-top-button"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import Popover from '../popover/popover.vue'
|
||||
import TimelineMenuContent from './timeline_menu_content.vue'
|
||||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||
import { ListsMenuContent } from '../lists_menu/lists_menu_content.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { TIMELINES } from 'src/components/navigation/navigation.js'
|
||||
import {
|
||||
faChevronDown
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
@ -22,11 +24,13 @@ export const timelineNames = () => {
|
|||
const TimelineMenu = {
|
||||
components: {
|
||||
Popover,
|
||||
TimelineMenuContent
|
||||
NavigationEntry,
|
||||
ListsMenuContent
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isOpen: false
|
||||
isOpen: false,
|
||||
timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k }))
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
@ -34,6 +38,12 @@ const TimelineMenu = {
|
|||
this.$store.dispatch('setLastTimeline', this.$route.name)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
useListsMenu () {
|
||||
const route = this.$route.name
|
||||
return route === 'lists-timeline'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openMenu () {
|
||||
// $nextTick is too fast, animation won't play back but
|
||||
|
|
|
@ -10,7 +10,19 @@
|
|||
@close="() => isOpen = false"
|
||||
>
|
||||
<template #content>
|
||||
<TimelineMenuContent />
|
||||
<ListsMenuContent
|
||||
v-if="useListsMenu"
|
||||
:show-pin="false"
|
||||
class="timelines"
|
||||
/>
|
||||
<ul v-else>
|
||||
<NavigationEntry
|
||||
v-for="item in timelinesList"
|
||||
:key="item.name"
|
||||
:show-pin="false"
|
||||
:item="item"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
<template #trigger>
|
||||
<span class="button-unstyled title timeline-menu-title">
|
||||
|
@ -138,8 +150,7 @@
|
|||
background-color: $fallback--lightBg;
|
||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||
color: $fallback--text;
|
||||
color: var(--selectedMenuText, $fallback--text);
|
||||
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
||||
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);
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import { mapState } from 'vuex'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faHome
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faUsers,
|
||||
faGlobe,
|
||||
faBookmark,
|
||||
faEnvelope,
|
||||
faHome
|
||||
)
|
||||
|
||||
const TimelineMenuContent = {
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
privateMode: state => state.instance.private,
|
||||
federating: state => state.instance.federating
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default TimelineMenuContent
|
|
@ -1,66 +0,0 @@
|
|||
<template>
|
||||
<ul>
|
||||
<li v-if="currentUser">
|
||||
<router-link
|
||||
class="menu-item"
|
||||
:to="{ name: 'friends' }"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding "
|
||||
icon="home"
|
||||
/>{{ $t("nav.home_timeline") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="currentUser || !privateMode">
|
||||
<router-link
|
||||
class="menu-item"
|
||||
:to="{ name: 'public-timeline' }"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding "
|
||||
icon="users"
|
||||
/>{{ $t("nav.public_tl") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="federating && (currentUser || !privateMode)">
|
||||
<router-link
|
||||
class="menu-item"
|
||||
:to="{ name: 'public-external-timeline' }"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding "
|
||||
icon="globe"
|
||||
/>{{ $t("nav.twkn") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="currentUser">
|
||||
<router-link
|
||||
class="menu-item"
|
||||
:to="{ name: 'bookmarks'}"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding "
|
||||
icon="bookmark"
|
||||
/>{{ $t("nav.bookmarks") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="currentUser">
|
||||
<router-link
|
||||
class="menu-item"
|
||||
:to="{ name: 'dms', params: { username: currentUser.screen_name } }"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding "
|
||||
icon="envelope"
|
||||
/>{{ $t("nav.dms") }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script src="./timeline_menu_content.js"></script>
|
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<FAIcon
|
||||
v-if="user && user.screen_name_ui_contains_non_ascii"
|
||||
icon="code"
|
||||
:title="$t('unicode_domain_indicator.tooltip')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faCode
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faCode
|
||||
)
|
||||
|
||||
const UnicodeDomainIndicator = {
|
||||
props: {
|
||||
user: Object
|
||||
}
|
||||
}
|
||||
|
||||
export default UnicodeDomainIndicator
|
||||
</script>
|
|
@ -38,7 +38,7 @@ const UpdateNotification = {
|
|||
return !this.$store.state.instance.disableUpdateNotification &&
|
||||
this.$store.state.users.currentUser &&
|
||||
this.$store.state.serverSideStorage.flagStorage.updateCounter < CURRENT_UPDATE_COUNTER &&
|
||||
!this.$store.state.serverSideStorage.flagStorage.dontShowUpdateNotifs
|
||||
!this.$store.state.serverSideStorage.prefsStorage.simple.dontShowUpdateNotifs
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -48,7 +48,7 @@ const UpdateNotification = {
|
|||
neverShowAgain () {
|
||||
this.toggleShow()
|
||||
this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER })
|
||||
this.$store.commit('setFlag', { flag: 'dontShowUpdateNotifs', value: 1 })
|
||||
this.$store.commit('setPreference', { path: 'simple.dontShowUpdateNotifs', value: true })
|
||||
this.$store.dispatch('pushServerSideStorage')
|
||||
},
|
||||
dismiss () {
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
<template #linkToArtist>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://post.ebin.club/pipivovott"
|
||||
href="https://post.ebin.club/users/pipivovott"
|
||||
>pipivovott</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
|
|
|
@ -5,6 +5,7 @@ import FollowButton from '../follow_button/follow_button.vue'
|
|||
import ModerationTools from '../moderation_tools/moderation_tools.vue'
|
||||
import AccountActions from '../account_actions/account_actions.vue'
|
||||
import Select from '../select/select.vue'
|
||||
import UserLink from '../user_link/user_link.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
@ -134,7 +135,8 @@ export default {
|
|||
ProgressButton,
|
||||
FollowButton,
|
||||
Select,
|
||||
RichContent
|
||||
RichContent,
|
||||
UserLink
|
||||
},
|
||||
methods: {
|
||||
muteUser () {
|
||||
|
|
|
@ -106,13 +106,10 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="bottom-line">
|
||||
<router-link
|
||||
<user-link
|
||||
class="user-screen-name"
|
||||
:title="user.screen_name_ui"
|
||||
:to="userProfileLink(user)"
|
||||
>
|
||||
@{{ user.screen_name_ui }}
|
||||
</router-link>
|
||||
:user="user"
|
||||
/>
|
||||
<template v-if="!hideBio">
|
||||
<span
|
||||
v-if="user.deactivated"
|
||||
|
|
38
src/components/user_link/user_link.vue
Normal file
38
src/components/user_link/user_link.vue
Normal file
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<router-link
|
||||
:title="user.screen_name_ui"
|
||||
:to="userProfileLink(user)"
|
||||
>
|
||||
{{ at ? '@' : '' }}{{ user.screen_name_ui }}<UnicodeDomainIndicator
|
||||
:user="user"
|
||||
/>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
const UserLink = {
|
||||
props: {
|
||||
user: Object,
|
||||
at: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
components: {
|
||||
UnicodeDomainIndicator
|
||||
},
|
||||
methods: {
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(
|
||||
user.id, user.screen_name,
|
||||
this.$store.state.instance.restrictedNicknames
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UserLink
|
||||
</script>
|
93
src/components/user_list_menu/user_list_menu.js
Normal file
93
src/components/user_list_menu/user_list_menu.js
Normal file
|
@ -0,0 +1,93 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import DialogModal from '../dialog_modal/dialog_modal.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
|
||||
library.add(faChevronRight)
|
||||
|
||||
const UserListMenu = {
|
||||
props: [
|
||||
'user'
|
||||
],
|
||||
data () {
|
||||
return {}
|
||||
},
|
||||
components: {
|
||||
DialogModal,
|
||||
Popover
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchUserInLists', this.user.id)
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
allLists: state => state.lists.allLists
|
||||
}),
|
||||
inListsSet () {
|
||||
return new Set(this.user.inLists.map(x => x.id))
|
||||
},
|
||||
lists () {
|
||||
if (!this.user.inLists) return []
|
||||
return this.allLists.map(list => ({
|
||||
...list,
|
||||
inList: this.inListsSet.has(list.id)
|
||||
}))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleList (listId) {
|
||||
if (this.inListsSet.has(listId)) {
|
||||
this.$store.dispatch('removeListAccount', { accountId: this.user.id, listId }).then((response) => {
|
||||
if (!response.ok) { return }
|
||||
this.$store.dispatch('fetchUserInLists', this.user.id)
|
||||
})
|
||||
} else {
|
||||
this.$store.dispatch('addListAccount', { accountId: this.user.id, listId }).then((response) => {
|
||||
if (!response.ok) { return }
|
||||
this.$store.dispatch('fetchUserInLists', this.user.id)
|
||||
})
|
||||
}
|
||||
},
|
||||
toggleRight (right) {
|
||||
const store = this.$store
|
||||
if (this.user.rights[right]) {
|
||||
store.state.api.backendInteractor.deleteRight({ user: this.user, right }).then(response => {
|
||||
if (!response.ok) { return }
|
||||
store.commit('updateRight', { user: this.user, right, value: false })
|
||||
})
|
||||
} else {
|
||||
store.state.api.backendInteractor.addRight({ user: this.user, right }).then(response => {
|
||||
if (!response.ok) { return }
|
||||
store.commit('updateRight', { user: this.user, right, value: true })
|
||||
})
|
||||
}
|
||||
},
|
||||
toggleActivationStatus () {
|
||||
this.$store.dispatch('toggleActivationStatus', { user: this.user })
|
||||
},
|
||||
deleteUserDialog (show) {
|
||||
this.showDeleteUserDialog = show
|
||||
},
|
||||
deleteUser () {
|
||||
const store = this.$store
|
||||
const user = this.user
|
||||
const { id, name } = user
|
||||
store.state.api.backendInteractor.deleteUser({ user })
|
||||
.then(e => {
|
||||
this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id)
|
||||
const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'
|
||||
const isTargetUser = this.$route.params.name === name || this.$route.params.id === id
|
||||
if (isProfile && isTargetUser) {
|
||||
window.history.back()
|
||||
}
|
||||
})
|
||||
},
|
||||
setToggled (value) {
|
||||
this.toggled = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UserListMenu
|
38
src/components/user_list_menu/user_list_menu.vue
Normal file
38
src/components/user_list_menu/user_list_menu.vue
Normal file
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<div class="UserListMenu">
|
||||
<Popover
|
||||
trigger="hover"
|
||||
placement="left"
|
||||
remove-padding
|
||||
>
|
||||
<template #content>
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
v-for="list in lists"
|
||||
:key="list.id"
|
||||
class="button-default dropdown-item"
|
||||
@click="toggleList(list.id)"
|
||||
>
|
||||
<span
|
||||
class="menu-checkbox"
|
||||
:class="{ 'menu-checkbox-checked': list.inList }"
|
||||
/>
|
||||
{{ list.title }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #trigger>
|
||||
<button class="btn button-default dropdown-item -has-submenu">
|
||||
{{ $t('lists.manage_lists') }}
|
||||
<FAIcon
|
||||
class="chevron-icon"
|
||||
size="lg"
|
||||
icon="chevron-right"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./user_list_menu.js"></script>
|
|
@ -1,5 +1,6 @@
|
|||
import { defineAsyncComponent } from 'vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
@ -15,6 +16,7 @@ const UserListPopover = {
|
|||
],
|
||||
components: {
|
||||
RichContent,
|
||||
UnicodeDomainIndicator,
|
||||
Popover: defineAsyncComponent(() => import('../popover/popover.vue')),
|
||||
UserAvatar: defineAsyncComponent(() => import('../user_avatar/user_avatar.vue'))
|
||||
},
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
:emoji="user.emoji"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<span class="user-list-screen-name">{{ user.screen_name_ui }}</span>
|
||||
<span class="user-list-screen-name">{{ user.screen_name_ui }}</span><UnicodeDomainIndicator :user="user" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -2,13 +2,15 @@ import Status from '../status/status.vue'
|
|||
import List from '../list/list.vue'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import Modal from '../modal/modal.vue'
|
||||
import UserLink from '../user_link/user_link.vue'
|
||||
|
||||
const UserReportingModal = {
|
||||
components: {
|
||||
Status,
|
||||
List,
|
||||
Checkbox,
|
||||
Modal
|
||||
Modal,
|
||||
UserLink
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
|
|
@ -5,9 +5,13 @@
|
|||
>
|
||||
<div class="user-reporting-panel panel">
|
||||
<div class="panel-heading">
|
||||
<div class="title">
|
||||
{{ $t('user_reporting.title', [user.screen_name_ui]) }}
|
||||
</div>
|
||||
<i18n-t
|
||||
tag="div"
|
||||
keypath="user_reporting.title"
|
||||
class="title"
|
||||
>
|
||||
<UserLink :user="user" />
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="user-reporting-panel-left">
|
||||
|
|
|
@ -80,11 +80,16 @@
|
|||
"confirm": "Confirm",
|
||||
"verify": "Verify",
|
||||
"close": "Close",
|
||||
"undo": "Undo",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"peek": "Peek",
|
||||
"role": {
|
||||
"admin": "Admin",
|
||||
"moderator": "Moderator"
|
||||
},
|
||||
"unpin": "Unpin item",
|
||||
"pin": "Pin item",
|
||||
"flash_content": "Click to show Flash content using Ruffle (Experimental, may not work).",
|
||||
"flash_security": "Note that this can be potentially dangerous since Flash content is still arbitrary code.",
|
||||
"flash_fail": "Failed to load flash content, see console for details.",
|
||||
|
@ -149,7 +154,10 @@
|
|||
"preferences": "Preferences",
|
||||
"timelines": "Timelines",
|
||||
"chats": "Chats",
|
||||
"lists": "Lists"
|
||||
"lists": "Lists",
|
||||
"edit_nav_mobile": "Customize navigation bar",
|
||||
"edit_pinned": "Edit pinned items",
|
||||
"edit_finish": "Done editing"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "Unknown status, searching for it…",
|
||||
|
@ -987,7 +995,18 @@
|
|||
"create": "Create",
|
||||
"save": "Save changes",
|
||||
"delete": "Delete list",
|
||||
"following_only": "Limit to Following"
|
||||
"following_only": "Limit to Following",
|
||||
"manage_lists": "Manage lists",
|
||||
"manage_members": "Manage list members",
|
||||
"add_members": "Search for more users",
|
||||
"remove_from_list": "Remove from list",
|
||||
"add_to_list": "Add to list",
|
||||
"is_in_list": "Already in list",
|
||||
"editing_list": "Editing list {listTitle}",
|
||||
"creating_list": "Creating new list",
|
||||
"update_title": "Save Title",
|
||||
"really_delete": "Really delete list?",
|
||||
"error": "Error manipulating lists: {0}"
|
||||
},
|
||||
"file_type": {
|
||||
"audio": "Audio",
|
||||
|
@ -1006,5 +1025,8 @@
|
|||
"update_changelog": "For more details on what's changed, see {theFullChangelog}.",
|
||||
"update_changelog_here": "the full changelog",
|
||||
"art_by": "Art by {linkToArtist}"
|
||||
},
|
||||
"unicode_domain_indicator": {
|
||||
"tooltip": "This domain contains non-ascii characters."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,9 @@ const api = {
|
|||
mastoUserSocketStatus: null,
|
||||
followRequests: []
|
||||
},
|
||||
getters: {
|
||||
followRequestCount: state => state.api.followRequests.length
|
||||
},
|
||||
mutations: {
|
||||
setBackendInteractor (state, backendInteractor) {
|
||||
state.backendInteractor = backendInteractor
|
||||
|
|
|
@ -89,7 +89,6 @@ export const defaultState = {
|
|||
contentColumnWidth: '45rem',
|
||||
notifsColumnWidth: '25rem',
|
||||
navbarColumnStretch: false,
|
||||
listsNavigation: false,
|
||||
greentext: undefined, // instance default
|
||||
useAtIcon: undefined, // instance default
|
||||
mentionLinkDisplay: undefined, // instance default
|
||||
|
|
|
@ -9,27 +9,43 @@ export const mutations = {
|
|||
setLists (state, value) {
|
||||
state.allLists = value
|
||||
},
|
||||
setList (state, { id, title }) {
|
||||
if (!state.allListsObject[id]) {
|
||||
state.allListsObject[id] = {}
|
||||
setList (state, { listId, title }) {
|
||||
if (!state.allListsObject[listId]) {
|
||||
state.allListsObject[listId] = { accountIds: [] }
|
||||
}
|
||||
state.allListsObject[id].title = title
|
||||
state.allListsObject[listId].title = title
|
||||
|
||||
if (!find(state.allLists, { id })) {
|
||||
state.allLists.push({ id, title })
|
||||
const entry = find(state.allLists, { id: listId })
|
||||
if (!entry) {
|
||||
state.allLists.push({ id: listId, title })
|
||||
} else {
|
||||
find(state.allLists, { id }).title = title
|
||||
entry.title = title
|
||||
}
|
||||
},
|
||||
setListAccounts (state, { id, accountIds }) {
|
||||
if (!state.allListsObject[id]) {
|
||||
state.allListsObject[id] = {}
|
||||
setListAccounts (state, { listId, accountIds }) {
|
||||
if (!state.allListsObject[listId]) {
|
||||
state.allListsObject[listId] = { accountIds: [] }
|
||||
}
|
||||
state.allListsObject[id].accountIds = accountIds
|
||||
state.allListsObject[listId].accountIds = accountIds
|
||||
},
|
||||
deleteList (state, { id }) {
|
||||
delete state.allListsObject[id]
|
||||
remove(state.allLists, list => list.id === id)
|
||||
addListAccount (state, { listId, accountId }) {
|
||||
if (!state.allListsObject[listId]) {
|
||||
state.allListsObject[listId] = { accountIds: [] }
|
||||
}
|
||||
state.allListsObject[listId].accountIds.push(accountId)
|
||||
},
|
||||
removeListAccount (state, { listId, accountId }) {
|
||||
if (!state.allListsObject[listId]) {
|
||||
state.allListsObject[listId] = { accountIds: [] }
|
||||
}
|
||||
const { accountIds } = state.allListsObject[listId]
|
||||
const set = new Set(accountIds)
|
||||
set.delete(accountId)
|
||||
state.allListsObject[listId].accountIds = [...set]
|
||||
},
|
||||
deleteList (state, { listId }) {
|
||||
delete state.allListsObject[listId]
|
||||
remove(state.allLists, list => list.id === listId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,37 +56,57 @@ const actions = {
|
|||
createList ({ rootState, commit }, { title }) {
|
||||
return rootState.api.backendInteractor.createList({ title })
|
||||
.then((list) => {
|
||||
commit('setList', { id: list.id, title })
|
||||
commit('setList', { listId: list.id, title })
|
||||
return list
|
||||
})
|
||||
},
|
||||
fetchList ({ rootState, commit }, { id }) {
|
||||
return rootState.api.backendInteractor.getList({ id })
|
||||
.then((list) => commit('setList', { id: list.id, title: list.title }))
|
||||
fetchList ({ rootState, commit }, { listId }) {
|
||||
return rootState.api.backendInteractor.getList({ listId })
|
||||
.then((list) => commit('setList', { listId: list.id, title: list.title }))
|
||||
},
|
||||
fetchListAccounts ({ rootState, commit }, { id }) {
|
||||
return rootState.api.backendInteractor.getListAccounts({ id })
|
||||
.then((accountIds) => commit('setListAccounts', { id, accountIds }))
|
||||
fetchListAccounts ({ rootState, commit }, { listId }) {
|
||||
return rootState.api.backendInteractor.getListAccounts({ listId })
|
||||
.then((accountIds) => commit('setListAccounts', { listId, accountIds }))
|
||||
},
|
||||
setList ({ rootState, commit }, { id, title }) {
|
||||
rootState.api.backendInteractor.updateList({ id, title })
|
||||
commit('setList', { id, title })
|
||||
setList ({ rootState, commit }, { listId, title }) {
|
||||
rootState.api.backendInteractor.updateList({ listId, title })
|
||||
commit('setList', { listId, title })
|
||||
},
|
||||
setListAccounts ({ rootState, commit }, { id, accountIds }) {
|
||||
const saved = rootState.lists.allListsObject[id].accountIds || []
|
||||
setListAccounts ({ rootState, commit }, { listId, accountIds }) {
|
||||
const saved = rootState.lists.allListsObject[listId].accountIds || []
|
||||
const added = accountIds.filter(id => !saved.includes(id))
|
||||
const removed = saved.filter(id => !accountIds.includes(id))
|
||||
commit('setListAccounts', { id, accountIds })
|
||||
commit('setListAccounts', { listId, accountIds })
|
||||
if (added.length > 0) {
|
||||
rootState.api.backendInteractor.addAccountsToList({ id, accountIds: added })
|
||||
rootState.api.backendInteractor.addAccountsToList({ listId, accountIds: added })
|
||||
}
|
||||
if (removed.length > 0) {
|
||||
rootState.api.backendInteractor.removeAccountsFromList({ id, accountIds: removed })
|
||||
rootState.api.backendInteractor.removeAccountsFromList({ listId, accountIds: removed })
|
||||
}
|
||||
},
|
||||
deleteList ({ rootState, commit }, { id }) {
|
||||
rootState.api.backendInteractor.deleteList({ id })
|
||||
commit('deleteList', { id })
|
||||
addListAccount ({ rootState, commit }, { listId, accountId }) {
|
||||
return rootState
|
||||
.api
|
||||
.backendInteractor
|
||||
.addAccountsToList({ listId, accountIds: [accountId] })
|
||||
.then((result) => {
|
||||
commit('addListAccount', { listId, accountId })
|
||||
return result
|
||||
})
|
||||
},
|
||||
removeListAccount ({ rootState, commit }, { listId, accountId }) {
|
||||
return rootState
|
||||
.api
|
||||
.backendInteractor
|
||||
.removeAccountsFromList({ listId, accountIds: [accountId] })
|
||||
.then((result) => {
|
||||
commit('removeListAccount', { listId, accountId })
|
||||
return result
|
||||
})
|
||||
},
|
||||
deleteList ({ rootState, commit }, { listId }) {
|
||||
rootState.api.backendInteractor.deleteList({ listId })
|
||||
commit('deleteList', { listId })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { toRaw } from 'vue'
|
||||
import { isEqual, cloneDeep } from 'lodash'
|
||||
import { isEqual, cloneDeep, set, get, clamp, flatten, groupBy, findLastIndex, takeRight } from 'lodash'
|
||||
import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js'
|
||||
|
||||
export const VERSION = 1
|
||||
|
@ -14,14 +14,21 @@ export const defaultState = {
|
|||
// storage of flags - stuff that can only be set and incremented
|
||||
flagStorage: {
|
||||
updateCounter: 0, // Counter for most recent update notification seen
|
||||
// TODO move to prefsStorage when that becomes a thing since only way
|
||||
// this can be reset is by complete reset of all flags
|
||||
dontShowUpdateNotifs: 0, // if user chose to not show update notifications ever again
|
||||
reset: 0 // special flag that can be used to force-reset all flags, debug purposes only
|
||||
// special reset codes:
|
||||
// 1000: trim keys to those known by currently running FE
|
||||
// 1001: same as above + reset everything to 0
|
||||
},
|
||||
prefsStorage: {
|
||||
_journal: [],
|
||||
simple: {
|
||||
dontShowUpdateNotifs: false,
|
||||
collapseNav: false
|
||||
},
|
||||
collections: {
|
||||
pinnedNavItems: ['home', 'dms', 'chats']
|
||||
}
|
||||
},
|
||||
// raw data
|
||||
raw: null,
|
||||
// local cache
|
||||
|
@ -33,14 +40,43 @@ export const newUserFlags = {
|
|||
updateCounter: CURRENT_UPDATE_COUNTER // new users don't need to see update notification
|
||||
}
|
||||
|
||||
const _wrapData = (data) => ({
|
||||
export const _moveItemInArray = (array, value, movement) => {
|
||||
const oldIndex = array.indexOf(value)
|
||||
const newIndex = oldIndex + movement
|
||||
const newArray = [...array]
|
||||
// remove old
|
||||
newArray.splice(oldIndex, 1)
|
||||
// add new
|
||||
newArray.splice(clamp(newIndex, 0, newArray.length + 1), 0, value)
|
||||
return newArray
|
||||
}
|
||||
|
||||
const _wrapData = (data, userName) => ({
|
||||
...data,
|
||||
_user: userName,
|
||||
_timestamp: Date.now(),
|
||||
_version: VERSION
|
||||
})
|
||||
|
||||
const _checkValidity = (data) => data._timestamp > 0 && data._version > 0
|
||||
|
||||
const _verifyPrefs = (state) => {
|
||||
state.prefsStorage = state.prefsStorage || {
|
||||
simple: {},
|
||||
collections: {}
|
||||
}
|
||||
Object.entries(defaultState.prefsStorage.simple).forEach(([k, v]) => {
|
||||
if (typeof v === 'number' || typeof v === 'boolean') return
|
||||
console.warn(`Preference simple.${k} as invalid type, reinitializing`)
|
||||
set(state.prefsStorage.simple, k, defaultState.prefsStorage.simple[k])
|
||||
})
|
||||
Object.entries(defaultState.prefsStorage.collections).forEach(([k, v]) => {
|
||||
if (Array.isArray(v)) return
|
||||
console.warn(`Preference collections.${k} as invalid type, reinitializing`)
|
||||
set(state.prefsStorage.collections, k, defaultState.prefsStorage.collections[k])
|
||||
})
|
||||
}
|
||||
|
||||
export const _getRecentData = (cache, live) => {
|
||||
const result = { recent: null, stale: null, needUpload: false }
|
||||
const cacheValid = _checkValidity(cache || {})
|
||||
|
@ -85,6 +121,8 @@ export const _getAllFlags = (recent, stale) => {
|
|||
}
|
||||
|
||||
export const _mergeFlags = (recent, stale, allFlagKeys) => {
|
||||
if (!stale.flagStorage) return recent.flagStorage
|
||||
if (!recent.flagStorage) return stale.flagStorage
|
||||
return Object.fromEntries(allFlagKeys.map(flag => {
|
||||
const recentFlag = recent.flagStorage[flag]
|
||||
const staleFlag = stale.flagStorage[flag]
|
||||
|
@ -93,6 +131,88 @@ export const _mergeFlags = (recent, stale, allFlagKeys) => {
|
|||
}))
|
||||
}
|
||||
|
||||
const _mergeJournal = (...journals) => {
|
||||
// Ignore invalid journal entries
|
||||
const allJournals = flatten(
|
||||
journals.map(j => Array.isArray(j) ? j : [])
|
||||
).filter(entry =>
|
||||
Object.prototype.hasOwnProperty.call(entry, 'path') &&
|
||||
Object.prototype.hasOwnProperty.call(entry, 'operation') &&
|
||||
Object.prototype.hasOwnProperty.call(entry, 'args') &&
|
||||
Object.prototype.hasOwnProperty.call(entry, 'timestamp')
|
||||
)
|
||||
const grouped = groupBy(allJournals, 'path')
|
||||
const trimmedGrouped = Object.entries(grouped).map(([path, journal]) => {
|
||||
// side effect
|
||||
journal.sort((a, b) => a.timestamp > b.timestamp ? 1 : -1)
|
||||
|
||||
if (path.startsWith('collections')) {
|
||||
const lastRemoveIndex = findLastIndex(journal, ({ operation }) => operation === 'removeFromCollection')
|
||||
// everything before last remove is unimportant
|
||||
if (lastRemoveIndex > 0) {
|
||||
return journal.slice(lastRemoveIndex)
|
||||
} else {
|
||||
// everything else doesn't need trimming
|
||||
return journal
|
||||
}
|
||||
} else if (path.startsWith('simple')) {
|
||||
// Only the last record is important
|
||||
return takeRight(journal)
|
||||
} else {
|
||||
return journal
|
||||
}
|
||||
})
|
||||
return flatten(trimmedGrouped)
|
||||
.sort((a, b) => a.timestamp > b.timestamp ? 1 : -1)
|
||||
}
|
||||
|
||||
export const _mergePrefs = (recent, stale, allFlagKeys) => {
|
||||
if (!stale) return recent
|
||||
if (!recent) return stale
|
||||
const { _journal: recentJournal, ...recentData } = recent
|
||||
const { _journal: staleJournal } = stale
|
||||
/** Journal entry format:
|
||||
* path: path to entry in prefsStorage
|
||||
* timestamp: timestamp of the change
|
||||
* operation: operation type
|
||||
* arguments: array of arguments, depends on operation type
|
||||
*
|
||||
* currently only supported operation type is "set" which just sets the value
|
||||
* to requested one. Intended only to be used with simple preferences (boolean, number)
|
||||
* shouldn't be used with collections!
|
||||
*/
|
||||
const resultOutput = { ...recentData }
|
||||
const totalJournal = _mergeJournal(staleJournal, recentJournal)
|
||||
totalJournal.forEach(({ path, timestamp, operation, command, args }) => {
|
||||
if (path.startsWith('_')) {
|
||||
console.error(`journal contains entry to edit internal (starts with _) field '${path}', something is incorrect here, ignoring.`)
|
||||
return
|
||||
}
|
||||
switch (operation) {
|
||||
case 'set':
|
||||
set(resultOutput, path, args[0])
|
||||
break
|
||||
case 'addToCollection':
|
||||
set(resultOutput, path, Array.from(new Set(get(resultOutput, path)).add(args[0])))
|
||||
break
|
||||
case 'removeFromCollection': {
|
||||
const newSet = new Set(get(resultOutput, path))
|
||||
newSet.delete(args[0])
|
||||
set(resultOutput, path, Array.from(newSet))
|
||||
break
|
||||
}
|
||||
case 'reorderCollection': {
|
||||
const [value, movement] = args
|
||||
set(resultOutput, path, _moveItemInArray(get(resultOutput, path), value, movement))
|
||||
break
|
||||
}
|
||||
default:
|
||||
console.error(`Unknown journal operation: '${operation}', did we forget to run reverse migrations beforehand?`)
|
||||
}
|
||||
})
|
||||
return { ...resultOutput, _journal: totalJournal }
|
||||
}
|
||||
|
||||
export const _resetFlags = (totalFlags, knownKeys = defaultState.flagStorage) => {
|
||||
let result = { ...totalFlags }
|
||||
const allFlagKeys = Object.keys(totalFlags)
|
||||
|
@ -149,10 +269,17 @@ export const _doMigrations = (cache) => {
|
|||
}
|
||||
|
||||
export const mutations = {
|
||||
clearServerSideStorage (state, userData) {
|
||||
state = { ...cloneDeep(defaultState) }
|
||||
},
|
||||
setServerSideStorage (state, userData) {
|
||||
const live = userData.storage
|
||||
state.raw = live
|
||||
let cache = state.cache
|
||||
if (cache && cache._user !== userData.fqn) {
|
||||
console.warn('cache belongs to another user! reinitializing local cache!')
|
||||
cache = null
|
||||
}
|
||||
|
||||
cache = _doMigrations(cache)
|
||||
|
||||
|
@ -165,7 +292,8 @@ export const mutations = {
|
|||
if (recent === null) {
|
||||
console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`)
|
||||
recent = _wrapData({
|
||||
flagStorage: { ...flagsTemplate }
|
||||
flagStorage: { ...flagsTemplate },
|
||||
prefsStorage: { ...defaultState.prefsStorage }
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -180,17 +308,23 @@ export const mutations = {
|
|||
|
||||
const allFlagKeys = _getAllFlags(recent, stale)
|
||||
let totalFlags
|
||||
let totalPrefs
|
||||
if (dirty) {
|
||||
// Merge the flags
|
||||
console.debug('Merging the flags...')
|
||||
console.debug('Merging the data...')
|
||||
totalFlags = _mergeFlags(recent, stale, allFlagKeys)
|
||||
_verifyPrefs(recent)
|
||||
_verifyPrefs(stale)
|
||||
totalPrefs = _mergePrefs(recent.prefsStorage, stale.prefsStorage)
|
||||
} else {
|
||||
totalFlags = recent.flagStorage
|
||||
totalPrefs = recent.prefsStorage
|
||||
}
|
||||
|
||||
totalFlags = _resetFlags(totalFlags)
|
||||
|
||||
recent.flagStorage = totalFlags
|
||||
recent.flagStorage = { ...flagsTemplate, ...totalFlags }
|
||||
recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs }
|
||||
|
||||
state.dirty = dirty || needsUpload
|
||||
state.cache = recent
|
||||
|
@ -199,10 +333,72 @@ export const mutations = {
|
|||
state.cache._timestamp = Math.min(stale._timestamp, recent._timestamp)
|
||||
}
|
||||
state.flagStorage = state.cache.flagStorage
|
||||
state.prefsStorage = state.cache.prefsStorage
|
||||
},
|
||||
setFlag (state, { flag, value }) {
|
||||
state.flagStorage[flag] = value
|
||||
state.dirty = true
|
||||
},
|
||||
setPreference (state, { path, value }) {
|
||||
if (path.startsWith('_')) {
|
||||
console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
|
||||
return
|
||||
}
|
||||
set(state.prefsStorage, path, value)
|
||||
state.prefsStorage._journal = [
|
||||
...state.prefsStorage._journal,
|
||||
{ operation: 'set', path, args: [value], timestamp: Date.now() }
|
||||
]
|
||||
state.dirty = true
|
||||
},
|
||||
addCollectionPreference (state, { path, value }) {
|
||||
if (path.startsWith('_')) {
|
||||
console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
|
||||
return
|
||||
}
|
||||
const collection = new Set(get(state.prefsStorage, path))
|
||||
collection.add(value)
|
||||
set(state.prefsStorage, path, [...collection])
|
||||
state.prefsStorage._journal = [
|
||||
...state.prefsStorage._journal,
|
||||
{ operation: 'addToCollection', path, args: [value], timestamp: Date.now() }
|
||||
]
|
||||
state.dirty = true
|
||||
},
|
||||
removeCollectionPreference (state, { path, value }) {
|
||||
if (path.startsWith('_')) {
|
||||
console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
|
||||
return
|
||||
}
|
||||
const collection = new Set(get(state.prefsStorage, path))
|
||||
collection.delete(value)
|
||||
set(state.prefsStorage, path, [...collection])
|
||||
state.prefsStorage._journal = [
|
||||
...state.prefsStorage._journal,
|
||||
{ operation: 'removeFromCollection', path, args: [value], timestamp: Date.now() }
|
||||
]
|
||||
state.dirty = true
|
||||
},
|
||||
reorderCollectionPreference (state, { path, value, movement }) {
|
||||
if (path.startsWith('_')) {
|
||||
console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
|
||||
return
|
||||
}
|
||||
const collection = get(state.prefsStorage, path)
|
||||
const newCollection = _moveItemInArray(collection, value, movement)
|
||||
set(state.prefsStorage, path, newCollection)
|
||||
state.prefsStorage._journal = [
|
||||
...state.prefsStorage._journal,
|
||||
{ operation: 'arrangeCollection', path, args: [value], timestamp: Date.now() }
|
||||
]
|
||||
state.dirty = true
|
||||
},
|
||||
updateCache (state, { username }) {
|
||||
state.prefsStorage._journal = _mergeJournal(state.prefsStorage._journal)
|
||||
state.cache = _wrapData({
|
||||
flagStorage: toRaw(state.flagStorage),
|
||||
prefsStorage: toRaw(state.prefsStorage)
|
||||
}, username)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -214,15 +410,16 @@ const serverSideStorage = {
|
|||
actions: {
|
||||
pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) {
|
||||
const needPush = state.dirty || force
|
||||
console.log(needPush)
|
||||
if (!needPush) return
|
||||
state.cache = _wrapData({
|
||||
flagStorage: toRaw(state.flagStorage)
|
||||
})
|
||||
commit('updateCache', { username: rootState.users.currentUser.fqn })
|
||||
const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } }
|
||||
rootState.api.backendInteractor
|
||||
.updateProfile({ params })
|
||||
.then((user) => commit('setServerSideStorage', user))
|
||||
state.dirty = false
|
||||
.then((user) => {
|
||||
commit('setServerSideStorage', user)
|
||||
state.dirty = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,6 +171,9 @@ export const mutations = {
|
|||
state.relationships[relationship.id] = relationship
|
||||
})
|
||||
},
|
||||
updateUserInLists (state, { id, inLists }) {
|
||||
state.usersObject[id].inLists = inLists
|
||||
},
|
||||
saveBlockIds (state, blockIds) {
|
||||
state.currentUser.blockIds = blockIds
|
||||
},
|
||||
|
@ -298,6 +301,12 @@ const users = {
|
|||
.then((relationships) => store.commit('updateUserRelationship', relationships))
|
||||
}
|
||||
},
|
||||
fetchUserInLists (store, id) {
|
||||
if (store.state.currentUser) {
|
||||
store.rootState.api.backendInteractor.fetchUserInLists({ id })
|
||||
.then((inLists) => store.commit('updateUserInLists', { id, inLists }))
|
||||
}
|
||||
},
|
||||
fetchBlocks (store) {
|
||||
return store.rootState.api.backendInteractor.fetchBlocks()
|
||||
.then((blocks) => {
|
||||
|
@ -509,6 +518,7 @@ const users = {
|
|||
store.dispatch('stopFetchingTimeline', 'friends')
|
||||
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
||||
store.dispatch('stopFetchingNotifications')
|
||||
store.dispatch('stopFetchingLists')
|
||||
store.dispatch('stopFetchingFollowRequests')
|
||||
store.commit('clearNotifications')
|
||||
store.commit('resetStatuses')
|
||||
|
@ -516,6 +526,7 @@ const users = {
|
|||
store.dispatch('setLastTimeline', 'public-timeline')
|
||||
store.dispatch('setLayoutWidth', windowWidth())
|
||||
store.dispatch('setLayoutHeight', windowHeight())
|
||||
store.commit('clearServerSideStorage')
|
||||
})
|
||||
},
|
||||
loginUser (store, accessToken) {
|
||||
|
@ -562,6 +573,12 @@ const users = {
|
|||
store.dispatch('startFetchingChats')
|
||||
}
|
||||
|
||||
store.dispatch('startFetchingLists')
|
||||
|
||||
if (user.locked) {
|
||||
store.dispatch('startFetchingFollowRequests')
|
||||
}
|
||||
|
||||
if (store.getters.mergedConfig.useStreamingApi) {
|
||||
store.dispatch('fetchTimeline', 'friends', { since: null })
|
||||
store.dispatch('fetchNotifications', { since: null })
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
.panel-footer {
|
||||
--panel-heading-height-padding: 0.6em;
|
||||
--__panel-heading-height: 3.2em;
|
||||
--__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding));
|
||||
--__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding, 0));
|
||||
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
@ -57,7 +57,7 @@
|
|||
grid-column-gap: 0.5em;
|
||||
flex: none;
|
||||
background-size: cover;
|
||||
padding: 0.6em;
|
||||
padding: var(--panel-heading-height-padding);
|
||||
height: var(--__panel-heading-height);
|
||||
line-height: var(--__panel-heading-height-inner);
|
||||
z-index: 4;
|
||||
|
@ -147,6 +147,15 @@
|
|||
color: var(--panelLink, $fallback--link);
|
||||
}
|
||||
|
||||
.button-unstyled:hover,
|
||||
a:hover {
|
||||
i[class*=icon-],
|
||||
.svg-inline--fa,
|
||||
.iconLetter {
|
||||
color: var(--panelText);
|
||||
}
|
||||
}
|
||||
|
||||
.faint {
|
||||
background-color: transparent;
|
||||
color: $fallback--faint;
|
||||
|
|
|
@ -53,6 +53,7 @@ const MASTODON_USER_URL = '/api/v1/accounts'
|
|||
const MASTODON_USER_LOOKUP_URL = '/api/v1/accounts/lookup'
|
||||
const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
|
||||
const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
|
||||
const MASTODON_USER_IN_LISTS = id => `/api/v1/accounts/${id}/lists`
|
||||
const MASTODON_LIST_URL = id => `/api/v1/lists/${id}`
|
||||
const MASTODON_LIST_TIMELINE_URL = id => `/api/v1/timelines/list/${id}`
|
||||
const MASTODON_LIST_ACCOUNTS_URL = id => `/api/v1/lists/${id}/accounts`
|
||||
|
@ -263,6 +264,13 @@ const unfollowUser = ({ id, credentials }) => {
|
|||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const fetchUserInLists = ({ id, credentials }) => {
|
||||
const url = MASTODON_USER_IN_LISTS(id)
|
||||
return fetch(url, {
|
||||
headers: authHeaders(credentials)
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const pinOwnStatus = ({ id, credentials }) => {
|
||||
return promisedRequest({ url: MASTODON_PIN_OWN_STATUS(id), credentials, method: 'POST' })
|
||||
.then((data) => parseStatus(data))
|
||||
|
@ -428,14 +436,14 @@ const createList = ({ title, credentials }) => {
|
|||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const getList = ({ id, credentials }) => {
|
||||
const url = MASTODON_LIST_URL(id)
|
||||
const getList = ({ listId, credentials }) => {
|
||||
const url = MASTODON_LIST_URL(listId)
|
||||
return fetch(url, { headers: authHeaders(credentials) })
|
||||
.then((data) => data.json())
|
||||
}
|
||||
|
||||
const updateList = ({ id, title, credentials }) => {
|
||||
const url = MASTODON_LIST_URL(id)
|
||||
const updateList = ({ listId, title, credentials }) => {
|
||||
const url = MASTODON_LIST_URL(listId)
|
||||
const headers = authHeaders(credentials)
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
|
@ -446,15 +454,15 @@ const updateList = ({ id, title, credentials }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const getListAccounts = ({ id, credentials }) => {
|
||||
const url = MASTODON_LIST_ACCOUNTS_URL(id)
|
||||
const getListAccounts = ({ listId, credentials }) => {
|
||||
const url = MASTODON_LIST_ACCOUNTS_URL(listId)
|
||||
return fetch(url, { headers: authHeaders(credentials) })
|
||||
.then((data) => data.json())
|
||||
.then((data) => data.map(({ id }) => id))
|
||||
}
|
||||
|
||||
const addAccountsToList = ({ id, accountIds, credentials }) => {
|
||||
const url = MASTODON_LIST_ACCOUNTS_URL(id)
|
||||
const addAccountsToList = ({ listId, accountIds, credentials }) => {
|
||||
const url = MASTODON_LIST_ACCOUNTS_URL(listId)
|
||||
const headers = authHeaders(credentials)
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
|
@ -465,8 +473,8 @@ const addAccountsToList = ({ id, accountIds, credentials }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const removeAccountsFromList = ({ id, accountIds, credentials }) => {
|
||||
const url = MASTODON_LIST_ACCOUNTS_URL(id)
|
||||
const removeAccountsFromList = ({ listId, accountIds, credentials }) => {
|
||||
const url = MASTODON_LIST_ACCOUNTS_URL(listId)
|
||||
const headers = authHeaders(credentials)
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
|
@ -477,8 +485,8 @@ const removeAccountsFromList = ({ id, accountIds, credentials }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const deleteList = ({ id, credentials }) => {
|
||||
const url = MASTODON_LIST_URL(id)
|
||||
const deleteList = ({ listId, credentials }) => {
|
||||
const url = MASTODON_LIST_URL(listId)
|
||||
return fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: authHeaders(credentials)
|
||||
|
@ -1584,7 +1592,8 @@ const apiService = {
|
|||
sendChatMessage,
|
||||
readChat,
|
||||
deleteChatMessage,
|
||||
setReportState
|
||||
setReportState,
|
||||
fetchUserInLists
|
||||
}
|
||||
|
||||
export default apiService
|
||||
|
|
|
@ -43,11 +43,13 @@ export const parseUser = (data) => {
|
|||
// case for users in "mentions" property for statuses in MastoAPI
|
||||
const mastoShort = masto && !Object.prototype.hasOwnProperty.call(data, 'avatar')
|
||||
|
||||
output.inLists = null
|
||||
output.id = String(data.id)
|
||||
output._original = data // used for server-side settings
|
||||
|
||||
if (masto) {
|
||||
output.screen_name = data.acct
|
||||
output.fqn = data.fqn
|
||||
output.statusnet_profile_url = data.url
|
||||
|
||||
// There's nothing else to get
|
||||
|
@ -214,12 +216,14 @@ export const parseUser = (data) => {
|
|||
output.screen_name_ui = output.screen_name
|
||||
if (output.screen_name && output.screen_name.includes('@')) {
|
||||
const parts = output.screen_name.split('@')
|
||||
let unicodeDomain = punycode.toUnicode(parts[1])
|
||||
const unicodeDomain = punycode.toUnicode(parts[1])
|
||||
if (unicodeDomain !== parts[1]) {
|
||||
// Add some identifier so users can potentially spot spoofing attempts:
|
||||
// lain.com and xn--lin-6cd.com would appear identical otherwise.
|
||||
unicodeDomain = '🌏' + unicodeDomain
|
||||
output.screen_name_ui_contains_non_ascii = true
|
||||
output.screen_name_ui = [parts[0], unicodeDomain].join('@')
|
||||
} else {
|
||||
output.screen_name_ui_contains_non_ascii = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,11 +17,6 @@ const webpackConfig = merge(baseConfig, {
|
|||
rules: utils.styleLoaders()
|
||||
},
|
||||
devtool: 'inline-source-map',
|
||||
// vue: {
|
||||
// loaders: {
|
||||
// js: 'isparta'
|
||||
// }
|
||||
// },
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': require('../../config/test.env')
|
||||
|
@ -37,22 +32,6 @@ const webpackConfig = merge(baseConfig, {
|
|||
// no need for app entry during tests
|
||||
delete webpackConfig.entry
|
||||
|
||||
// make sure isparta loader is applied before eslint
|
||||
// webpackConfig.module.preLoaders = webpackConfig.module.preLoaders || []
|
||||
// webpackConfig.module.preLoaders.unshift({
|
||||
// test: /\.js$/,
|
||||
// loader: 'isparta',
|
||||
// include: path.resolve(projectRoot, 'src')
|
||||
// })
|
||||
|
||||
// // only apply babel for test files when using isparta
|
||||
// webpackConfig.module.loaders.some(function (loader, i) {
|
||||
// if (loader.loader === 'babel') {
|
||||
// loader.include = path.resolve(projectRoot, 'test/unit')
|
||||
// return true
|
||||
// }
|
||||
// })
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
// to run in additional browsers:
|
||||
|
|
|
@ -17,13 +17,13 @@ describe('The lists module', () => {
|
|||
const list = { id: '1', title: 'testList' }
|
||||
const modList = { id: '1', title: 'anotherTestTitle' }
|
||||
|
||||
mutations.setList(state, list)
|
||||
expect(state.allListsObject[list.id]).to.eql({ title: list.title })
|
||||
mutations.setList(state, { listId: list.id, title: list.title })
|
||||
expect(state.allListsObject[list.id]).to.eql({ title: list.title, accountIds: [] })
|
||||
expect(state.allLists).to.have.length(1)
|
||||
expect(state.allLists[0]).to.eql(list)
|
||||
|
||||
mutations.setList(state, modList)
|
||||
expect(state.allListsObject[modList.id]).to.eql({ title: modList.title })
|
||||
mutations.setList(state, { listId: modList.id, title: modList.title })
|
||||
expect(state.allListsObject[modList.id]).to.eql({ title: modList.title, accountIds: [] })
|
||||
expect(state.allLists).to.have.length(1)
|
||||
expect(state.allLists[0]).to.eql(modList)
|
||||
})
|
||||
|
@ -33,10 +33,10 @@ describe('The lists module', () => {
|
|||
const list = { id: '1', accountIds: ['1', '2', '3'] }
|
||||
const modList = { id: '1', accountIds: ['3', '4', '5'] }
|
||||
|
||||
mutations.setListAccounts(state, list)
|
||||
mutations.setListAccounts(state, { listId: list.id, accountIds: list.accountIds })
|
||||
expect(state.allListsObject[list.id]).to.eql({ accountIds: list.accountIds })
|
||||
|
||||
mutations.setListAccounts(state, modList)
|
||||
mutations.setListAccounts(state, { listId: modList.id, accountIds: modList.accountIds })
|
||||
expect(state.allListsObject[modList.id]).to.eql({ accountIds: modList.accountIds })
|
||||
})
|
||||
|
||||
|
@ -47,9 +47,9 @@ describe('The lists module', () => {
|
|||
1: { title: 'testList', accountIds: ['1', '2', '3'] }
|
||||
}
|
||||
}
|
||||
const id = '1'
|
||||
const listId = '1'
|
||||
|
||||
mutations.deleteList(state, { id })
|
||||
mutations.deleteList(state, { listId })
|
||||
expect(state.allLists).to.have.length(0)
|
||||
expect(state.allListsObject).to.eql({})
|
||||
})
|
||||
|
|
|
@ -4,9 +4,11 @@ import {
|
|||
VERSION,
|
||||
COMMAND_TRIM_FLAGS,
|
||||
COMMAND_TRIM_FLAGS_AND_RESET,
|
||||
_moveItemInArray,
|
||||
_getRecentData,
|
||||
_getAllFlags,
|
||||
_mergeFlags,
|
||||
_mergePrefs,
|
||||
_resetFlags,
|
||||
mutations,
|
||||
defaultState,
|
||||
|
@ -28,6 +30,7 @@ describe('The serverSideStorage module', () => {
|
|||
expect(state.cache._version).to.eql(VERSION)
|
||||
expect(state.cache._timestamp).to.be.a('number')
|
||||
expect(state.cache.flagStorage).to.eql(defaultState.flagStorage)
|
||||
expect(state.cache.prefsStorage).to.eql(defaultState.prefsStorage)
|
||||
})
|
||||
|
||||
it('should initialize storage with proper flags for new users if none present', () => {
|
||||
|
@ -36,6 +39,7 @@ describe('The serverSideStorage module', () => {
|
|||
expect(state.cache._version).to.eql(VERSION)
|
||||
expect(state.cache._timestamp).to.be.a('number')
|
||||
expect(state.cache.flagStorage).to.eql(newUserFlags)
|
||||
expect(state.cache.prefsStorage).to.eql(defaultState.prefsStorage)
|
||||
})
|
||||
|
||||
it('should merge flags even if remote timestamp is older', () => {
|
||||
|
@ -57,6 +61,9 @@ describe('The serverSideStorage module', () => {
|
|||
flagStorage: {
|
||||
...defaultState.flagStorage,
|
||||
updateCounter: 1
|
||||
},
|
||||
prefsStorage: {
|
||||
...defaultState.prefsStorage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,9 +106,62 @@ describe('The serverSideStorage module', () => {
|
|||
expect(state.cache.flagStorage).to.eql(defaultState.flagStorage)
|
||||
})
|
||||
})
|
||||
describe('setPreference', () => {
|
||||
const { setPreference, updateCache, addCollectionPreference, removeCollectionPreference } = mutations
|
||||
|
||||
it('should set preference and update journal log accordingly', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
setPreference(state, { path: 'simple.testing', value: 1 })
|
||||
expect(state.prefsStorage.simple.testing).to.eql(1)
|
||||
expect(state.prefsStorage._journal.length).to.eql(1)
|
||||
expect(state.prefsStorage._journal[0]).to.eql({
|
||||
path: 'simple.testing',
|
||||
operation: 'set',
|
||||
args: [1],
|
||||
// should have A timestamp, we don't really care what it is
|
||||
timestamp: state.prefsStorage._journal[0].timestamp
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep journal to a minimum', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
setPreference(state, { path: 'simple.testing', value: 1 })
|
||||
setPreference(state, { path: 'simple.testing', value: 2 })
|
||||
addCollectionPreference(state, { path: 'collections.testing', value: 2 })
|
||||
removeCollectionPreference(state, { path: 'collections.testing', value: 2 })
|
||||
updateCache(state, { username: 'test' })
|
||||
expect(state.prefsStorage.simple.testing).to.eql(2)
|
||||
expect(state.prefsStorage.collections.testing).to.eql([])
|
||||
expect(state.prefsStorage._journal.length).to.eql(2)
|
||||
expect(state.prefsStorage._journal[0]).to.eql({
|
||||
path: 'simple.testing',
|
||||
operation: 'set',
|
||||
args: [2],
|
||||
// should have A timestamp, we don't really care what it is
|
||||
timestamp: state.prefsStorage._journal[0].timestamp
|
||||
})
|
||||
expect(state.prefsStorage._journal[1]).to.eql({
|
||||
path: 'collections.testing',
|
||||
operation: 'removeFromCollection',
|
||||
args: [2],
|
||||
// should have A timestamp, we don't really care what it is
|
||||
timestamp: state.prefsStorage._journal[1].timestamp
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('helper functions', () => {
|
||||
describe('_moveItemInArray', () => {
|
||||
it('should move item according to movement value', () => {
|
||||
expect(_moveItemInArray([1, 2, 3, 4], 4, -1)).to.eql([1, 2, 4, 3])
|
||||
expect(_moveItemInArray([1, 2, 3, 4], 1, 2)).to.eql([2, 3, 1, 4])
|
||||
})
|
||||
it('should clamp movement to within array', () => {
|
||||
expect(_moveItemInArray([1, 2, 3, 4], 4, -10)).to.eql([4, 1, 2, 3])
|
||||
expect(_moveItemInArray([1, 2, 3, 4], 3, 99)).to.eql([1, 2, 4, 3])
|
||||
})
|
||||
})
|
||||
describe('_getRecentData', () => {
|
||||
it('should handle nulls correctly', () => {
|
||||
expect(_getRecentData(null, null)).to.eql({ recent: null, stale: null, needUpload: true })
|
||||
|
@ -157,6 +217,94 @@ describe('The serverSideStorage module', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('_mergePrefs', () => {
|
||||
it('should prefer recent and apply journal to it', () => {
|
||||
expect(
|
||||
_mergePrefs(
|
||||
// RECENT
|
||||
{
|
||||
simple: { a: 1, b: 0, c: true },
|
||||
_journal: [
|
||||
{ path: 'simple.b', operation: 'set', args: [0], timestamp: 2 },
|
||||
{ path: 'simple.c', operation: 'set', args: [true], timestamp: 4 }
|
||||
]
|
||||
},
|
||||
// STALE
|
||||
{
|
||||
simple: { a: 1, b: 1, c: false },
|
||||
_journal: [
|
||||
{ path: 'simple.a', operation: 'set', args: [1], timestamp: 1 },
|
||||
{ path: 'simple.b', operation: 'set', args: [1], timestamp: 3 }
|
||||
]
|
||||
}
|
||||
)
|
||||
).to.eql({
|
||||
simple: { a: 1, b: 1, c: true },
|
||||
_journal: [
|
||||
{ path: 'simple.a', operation: 'set', args: [1], timestamp: 1 },
|
||||
{ path: 'simple.b', operation: 'set', args: [1], timestamp: 3 },
|
||||
{ path: 'simple.c', operation: 'set', args: [true], timestamp: 4 }
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow setting falsy values', () => {
|
||||
expect(
|
||||
_mergePrefs(
|
||||
// RECENT
|
||||
{
|
||||
simple: { a: 1, b: 0, c: false },
|
||||
_journal: [
|
||||
{ path: 'simple.b', operation: 'set', args: [0], timestamp: 2 },
|
||||
{ path: 'simple.c', operation: 'set', args: [false], timestamp: 4 }
|
||||
]
|
||||
},
|
||||
// STALE
|
||||
{
|
||||
simple: { a: 0, b: 0, c: true },
|
||||
_journal: [
|
||||
{ path: 'simple.a', operation: 'set', args: [0], timestamp: 1 },
|
||||
{ path: 'simple.b', operation: 'set', args: [0], timestamp: 3 }
|
||||
]
|
||||
}
|
||||
)
|
||||
).to.eql({
|
||||
simple: { a: 0, b: 0, c: false },
|
||||
_journal: [
|
||||
{ path: 'simple.a', operation: 'set', args: [0], timestamp: 1 },
|
||||
{ path: 'simple.b', operation: 'set', args: [0], timestamp: 3 },
|
||||
{ path: 'simple.c', operation: 'set', args: [false], timestamp: 4 }
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it('should work with strings', () => {
|
||||
expect(
|
||||
_mergePrefs(
|
||||
// RECENT
|
||||
{
|
||||
simple: { a: 'foo' },
|
||||
_journal: [
|
||||
{ path: 'simple.a', operation: 'set', args: ['foo'], timestamp: 2 }
|
||||
]
|
||||
},
|
||||
// STALE
|
||||
{
|
||||
simple: { a: 'bar' },
|
||||
_journal: [
|
||||
{ path: 'simple.a', operation: 'set', args: ['bar'], timestamp: 4 }
|
||||
]
|
||||
}
|
||||
)
|
||||
).to.eql({
|
||||
simple: { a: 'bar' },
|
||||
_journal: [
|
||||
{ path: 'simple.a', operation: 'set', args: ['bar'], timestamp: 4 }
|
||||
]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('_resetFlags', () => {
|
||||
it('should reset all known flags to 0 when reset flag is set to > 0 and < 9000', () => {
|
||||
const totalFlags = { a: 0, b: 3, reset: 1 }
|
||||
|
|
|
@ -269,7 +269,8 @@ describe('API Entities normalizer', () => {
|
|||
it('converts IDN to unicode and marks it as internatonal', () => {
|
||||
const user = makeMockUserMasto({ acct: 'lain@xn--lin-6cd.com' })
|
||||
|
||||
expect(parseUser(user)).to.have.property('screen_name_ui').that.equal('lain@🌏lаin.com')
|
||||
expect(parseUser(user)).to.have.property('screen_name_ui').that.equal('lain@lаin.com')
|
||||
expect(parseUser(user)).to.have.property('screen_name_ui_contains_non_ascii').that.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in a new issue