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
17
package.json
17
package.json
|
@ -43,19 +43,19 @@
|
||||||
"utf8": "3.0.0",
|
"utf8": "3.0.0",
|
||||||
"vue": "3.2.37",
|
"vue": "3.2.37",
|
||||||
"vue-i18n": "9.2.2",
|
"vue-i18n": "9.2.2",
|
||||||
"vue-router": "4.1.3",
|
"vue-router": "4.1.5",
|
||||||
"vue-template-compiler": "2.7.9",
|
"vue-template-compiler": "2.7.10",
|
||||||
"vuex": "4.0.2"
|
"vuex": "4.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.18.10",
|
"@babel/core": "7.18.13",
|
||||||
"@babel/eslint-parser": "7.18.9",
|
"@babel/eslint-parser": "7.18.9",
|
||||||
"@babel/plugin-transform-runtime": "7.18.10",
|
"@babel/plugin-transform-runtime": "7.18.10",
|
||||||
"@babel/preset-env": "7.18.10",
|
"@babel/preset-env": "7.18.10",
|
||||||
"@babel/register": "7.18.9",
|
"@babel/register": "7.18.9",
|
||||||
"@intlify/vue-i18n-loader": "5.0.0",
|
"@intlify/vue-i18n-loader": "5.0.0",
|
||||||
"@ungap/event-target": "0.2.3",
|
"@ungap/event-target": "0.2.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/babel-plugin-jsx": "1.1.1",
|
||||||
"@vue/compiler-sfc": "3.2.37",
|
"@vue/compiler-sfc": "3.2.37",
|
||||||
"@vue/test-utils": "2.0.2",
|
"@vue/test-utils": "2.0.2",
|
||||||
|
@ -71,12 +71,12 @@
|
||||||
"css-loader": "6.7.1",
|
"css-loader": "6.7.1",
|
||||||
"css-minimizer-webpack-plugin": "4.0.0",
|
"css-minimizer-webpack-plugin": "4.0.0",
|
||||||
"custom-event-polyfill": "1.0.7",
|
"custom-event-polyfill": "1.0.7",
|
||||||
"eslint": "8.22.0",
|
"eslint": "8.23.0",
|
||||||
"eslint-config-standard": "17.0.0",
|
"eslint-config-standard": "17.0.0",
|
||||||
"eslint-formatter-friendly": "7.0.0",
|
"eslint-formatter-friendly": "7.0.0",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"eslint-plugin-n": "15.2.5",
|
"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-plugin-vue": "9.4.0",
|
||||||
"eslint-webpack-plugin": "3.2.0",
|
"eslint-webpack-plugin": "3.2.0",
|
||||||
"eventsource-polyfill": "0.9.6",
|
"eventsource-polyfill": "0.9.6",
|
||||||
|
@ -85,7 +85,6 @@
|
||||||
"html-webpack-plugin": "5.5.0",
|
"html-webpack-plugin": "5.5.0",
|
||||||
"http-proxy-middleware": "2.0.6",
|
"http-proxy-middleware": "2.0.6",
|
||||||
"iso-639-1": "2.1.15",
|
"iso-639-1": "2.1.15",
|
||||||
"isparta-loader": "2.0.0",
|
|
||||||
"json-loader": "0.5.7",
|
"json-loader": "0.5.7",
|
||||||
"karma": "6.4.0",
|
"karma": "6.4.0",
|
||||||
"karma-coverage": "2.2.0",
|
"karma-coverage": "2.2.0",
|
||||||
|
@ -108,11 +107,11 @@
|
||||||
"sass": "1.54.5",
|
"sass": "1.54.5",
|
||||||
"sass-loader": "13.0.2",
|
"sass-loader": "13.0.2",
|
||||||
"selenium-server": "2.53.1",
|
"selenium-server": "2.53.1",
|
||||||
"semver": "5.7.1",
|
"semver": "7.3.7",
|
||||||
"serviceworker-webpack5-plugin": "2.0.0",
|
"serviceworker-webpack5-plugin": "2.0.0",
|
||||||
"shelljs": "0.8.5",
|
"shelljs": "0.8.5",
|
||||||
"sinon": "2.4.1",
|
"sinon": "2.4.1",
|
||||||
"sinon-chai": "2.14.0",
|
"sinon-chai": "3.7.0",
|
||||||
"stylelint": "13.13.1",
|
"stylelint": "13.13.1",
|
||||||
"stylelint-config-standard": "20.0.0",
|
"stylelint-config-standard": "20.0.0",
|
||||||
"stylelint-rscss": "0.4.0",
|
"stylelint-rscss": "0.4.0",
|
||||||
|
|
|
@ -92,8 +92,12 @@ export default {
|
||||||
isChats () {
|
isChats () {
|
||||||
return this.$route.name === 'chat' || this.$route.name === 'chats'
|
return this.$route.name === 'chat' || this.$route.name === 'chats'
|
||||||
},
|
},
|
||||||
|
isListEdit () {
|
||||||
|
return this.$route.name === 'lists-edit'
|
||||||
|
},
|
||||||
newPostButtonShown () {
|
newPostButtonShown () {
|
||||||
if (this.isChats) return false
|
if (this.isChats) return false
|
||||||
|
if (this.isListEdit) return false
|
||||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
||||||
},
|
},
|
||||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||||
|
|
26
src/App.scss
26
src/App.scss
|
@ -117,12 +117,28 @@ h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.iconLetter {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
i[class*=icon-],
|
i[class*=icon-],
|
||||||
.svg-inline--fa {
|
.svg-inline--fa,
|
||||||
|
.iconLetter {
|
||||||
color: $fallback--icon;
|
color: $fallback--icon;
|
||||||
color: var(--icon, $fallback--icon);
|
color: var(--icon, $fallback--icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-unstyled:hover,
|
||||||
|
a:hover {
|
||||||
|
> i[class*=icon-],
|
||||||
|
> .svg-inline--fa,
|
||||||
|
> .iconLetter {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
z-index: var(--ZI_navbar);
|
z-index: var(--ZI_navbar);
|
||||||
color: var(--topBarText);
|
color: var(--topBarText);
|
||||||
|
@ -765,17 +781,23 @@ option {
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-scale-110 {
|
.fa-scale-110 {
|
||||||
&.svg-inline--fa {
|
&.svg-inline--fa,
|
||||||
|
&.iconLetter {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-old-padding {
|
.fa-old-padding {
|
||||||
|
&.iconLetter,
|
||||||
&.svg-inline--fa, &-layer {
|
&.svg-inline--fa, &-layer {
|
||||||
padding: 0 0.3em;
|
padding: 0 0.3em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.veryfaint {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
.login-hint {
|
.login-hint {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<div
|
<div
|
||||||
id="main-scroller"
|
id="main-scroller"
|
||||||
class="column main"
|
class="column main"
|
||||||
:class="{ '-full-height': isChats }"
|
:class="{ '-full-height': isChats || isListEdit }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!currentUser"
|
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 Lists from 'components/lists/lists.vue'
|
||||||
import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
|
import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
|
||||||
import ListsEdit from 'components/lists_edit/lists_edit.vue'
|
import ListsEdit from 'components/lists_edit/lists_edit.vue'
|
||||||
|
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
||||||
|
|
||||||
export default (store) => {
|
export default (store) => {
|
||||||
const validateAuthenticatedRoute = (to, from, next) => {
|
const validateAuthenticatedRoute = (to, from, next) => {
|
||||||
|
@ -79,7 +80,9 @@ export default (store) => {
|
||||||
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile },
|
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile },
|
||||||
{ name: 'lists', path: '/lists', component: Lists },
|
{ name: 'lists', path: '/lists', component: Lists },
|
||||||
{ name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline },
|
{ 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) {
|
if (store.state.instance.pleromaChatMessagesAvailable) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import ProgressButton from '../progress_button/progress_button.vue'
|
import ProgressButton from '../progress_button/progress_button.vue'
|
||||||
import Popover from '../popover/popover.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 { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faEllipsisV
|
faEllipsisV
|
||||||
|
@ -19,7 +20,8 @@ const AccountActions = {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ProgressButton,
|
ProgressButton,
|
||||||
Popover
|
Popover,
|
||||||
|
UserListMenu
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showRepeats () {
|
showRepeats () {
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
class="dropdown-divider"
|
class="dropdown-divider"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
<UserListMenu :user="user" />
|
||||||
<button
|
<button
|
||||||
v-if="relationship.blocking"
|
v-if="relationship.blocking"
|
||||||
class="btn button-default btn-block dropdown-item"
|
class="btn button-default btn-block dropdown-item"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import UserPopover from '../user_popover/user_popover.vue'
|
import UserPopover from '../user_popover/user_popover.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.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 RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
|
||||||
|
@ -10,7 +11,8 @@ const BasicUserCard = {
|
||||||
components: {
|
components: {
|
||||||
UserPopover,
|
UserPopover,
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
RichContent
|
RichContent,
|
||||||
|
UserLink
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
userProfileLink (user) {
|
userProfileLink (user) {
|
||||||
|
|
|
@ -30,12 +30,10 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<router-link
|
<user-link
|
||||||
class="basic-user-card-screen-name"
|
class="basic-user-card-screen-name"
|
||||||
:to="userProfileLink(user)"
|
:user="user"
|
||||||
>
|
/>
|
||||||
@{{ user.screen_name_ui }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -137,4 +137,8 @@
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
:title="$t('nav.administration')"
|
:title="$t('nav.administration')"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
<span class="spacer" />
|
||||||
<button
|
<button
|
||||||
v-if="currentUser"
|
v-if="currentUser"
|
||||||
class="button-unstyled nav-icon"
|
class="button-unstyled nav-icon"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Completion from '../../services/completion/completion.js'
|
import Completion from '../../services/completion/completion.js'
|
||||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||||
|
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||||
import { take } from 'lodash'
|
import { take } from 'lodash'
|
||||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||||
|
|
||||||
|
@ -120,7 +121,8 @@ const EmojiInput = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
EmojiPicker
|
EmojiPicker,
|
||||||
|
UnicodeDomainIndicator
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
padEmoji () {
|
padEmoji () {
|
||||||
|
|
|
@ -50,7 +50,21 @@
|
||||||
<span v-else>{{ suggestion.replacement }}</span>
|
<span v-else>{{ suggestion.replacement }}</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="label">
|
<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>
|
<span class="detailText">{{ suggestion.detailText }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -116,11 +116,12 @@ export const suggestUsers = ({ dispatch, state }) => {
|
||||||
|
|
||||||
return diff + nameAlphabetically + screenNameAlphabetically
|
return diff + nameAlphabetically + screenNameAlphabetically
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
|
}).map((user) => ({
|
||||||
displayText: screen_name_ui,
|
user,
|
||||||
detailText: name,
|
displayText: user.screen_name_ui,
|
||||||
imageUrl: profile_image_url_original,
|
detailText: user.name,
|
||||||
replacement: '@' + screen_name + ' '
|
imageUrl: user.profile_image_url_original,
|
||||||
|
replacement: '@' + user.screen_name + ' '
|
||||||
}))
|
}))
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import ListsCard from '../lists_card/lists_card.vue'
|
import ListsCard from '../lists_card/lists_card.vue'
|
||||||
import ListsNew from '../lists_new/lists_new.vue'
|
|
||||||
|
|
||||||
const Lists = {
|
const Lists = {
|
||||||
data () {
|
data () {
|
||||||
|
@ -8,11 +7,7 @@ const Lists = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ListsCard,
|
ListsCard
|
||||||
ListsNew
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.$store.dispatch('startFetchingLists')
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
lists () {
|
lists () {
|
||||||
|
|
|
@ -1,21 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="isNew">
|
<div class="Lists panel panel-default">
|
||||||
<ListsNew @cancel="cancelNewList" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="settings panel panel-default"
|
|
||||||
>
|
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{{ $t('lists.lists') }}
|
{{ $t('lists.lists') }}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<router-link
|
||||||
class="button-default"
|
:to="{ name: 'lists-new' }"
|
||||||
@click="newList"
|
class="button-default btn new-list-button"
|
||||||
>
|
>
|
||||||
{{ $t("lists.new") }}
|
{{ $t("lists.new") }}
|
||||||
</button>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<ListsCard
|
<ListsCard
|
||||||
|
@ -29,3 +23,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./lists.js"></script>
|
<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 { mapState, mapGetters } from 'vuex'
|
||||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||||
import ListsUserSearch from '../lists_user_search/lists_user_search.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 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 { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faSearch,
|
faSearch,
|
||||||
|
@ -17,22 +19,33 @@ const ListsNew = {
|
||||||
components: {
|
components: {
|
||||||
BasicUserCard,
|
BasicUserCard,
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
ListsUserSearch
|
ListsUserSearch,
|
||||||
|
TabSwitcher,
|
||||||
|
PanelLoading
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
title: '',
|
title: '',
|
||||||
userIds: [],
|
titleDraft: '',
|
||||||
selectedUserIds: []
|
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 () {
|
created () {
|
||||||
this.$store.dispatch('fetchList', { id: this.id })
|
if (!this.id) return
|
||||||
.then(() => { this.title = this.findListTitle(this.id) })
|
this.$store.dispatch('fetchList', { listId: this.id })
|
||||||
this.$store.dispatch('fetchListAccounts', { id: this.id })
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.selectedUserIds = this.findListAccounts(this.id)
|
this.title = this.findListTitle(this.id)
|
||||||
this.selectedUserIds.forEach(userId => {
|
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)
|
this.$store.dispatch('fetchUserIfMissing', userId)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -41,11 +54,12 @@ const ListsNew = {
|
||||||
id () {
|
id () {
|
||||||
return this.$route.params.id
|
return this.$route.params.id
|
||||||
},
|
},
|
||||||
users () {
|
membersUsers () {
|
||||||
return this.userIds.map(userId => this.findUser(userId))
|
return [...this.membersUserIds, ...this.addedUserIds]
|
||||||
|
.map(userId => this.findUser(userId)).filter(user => user)
|
||||||
},
|
},
|
||||||
selectedUsers () {
|
searchUsers () {
|
||||||
return this.selectedUserIds.map(userId => this.findUser(userId)).filter(user => user)
|
return this.searchUserIds.map(userId => this.findUser(userId)).filter(user => user)
|
||||||
},
|
},
|
||||||
...mapState({
|
...mapState({
|
||||||
currentUser: state => state.users.currentUser
|
currentUser: state => state.users.currentUser
|
||||||
|
@ -56,33 +70,73 @@ const ListsNew = {
|
||||||
onInput () {
|
onInput () {
|
||||||
this.search(this.query)
|
this.search(this.query)
|
||||||
},
|
},
|
||||||
selectUser (user) {
|
toggleRemoveMember (user) {
|
||||||
if (this.selectedUserIds.includes(user.id)) {
|
if (this.removedUserIds.has(user.id)) {
|
||||||
this.removeUser(user.id)
|
this.id && this.addUser(user)
|
||||||
|
this.removedUserIds.delete(user.id)
|
||||||
} else {
|
} else {
|
||||||
this.addUser(user)
|
this.id && this.removeUser(user.id)
|
||||||
|
this.removedUserIds.add(user.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isSelected (user) {
|
toggleAddFromSearch (user) {
|
||||||
return this.selectedUserIds.includes(user.id)
|
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) {
|
addUser (user) {
|
||||||
this.selectedUserIds.push(user.id)
|
this.$store.dispatch('addListAccount', { accountId: this.user.id, listId: this.id })
|
||||||
},
|
},
|
||||||
removeUser (userId) {
|
removeUser (userId) {
|
||||||
this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId)
|
this.$store.dispatch('removeListAccount', { accountId: this.user.id, listId: this.id })
|
||||||
},
|
},
|
||||||
onResults (results) {
|
onSearchLoading (results) {
|
||||||
this.userIds = results
|
this.searchLoading = true
|
||||||
},
|
},
|
||||||
updateList () {
|
onSearchLoadingDone (results) {
|
||||||
this.$store.dispatch('setList', { id: this.id, title: this.title })
|
this.searchLoading = false
|
||||||
this.$store.dispatch('setListAccounts', { id: this.id, accountIds: this.selectedUserIds })
|
},
|
||||||
|
onSearchResults (results) {
|
||||||
this.$router.push({ name: 'lists-timeline', params: { id: this.id } })
|
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 () {
|
deleteList () {
|
||||||
this.$store.dispatch('deleteList', { id: this.id })
|
this.$store.dispatch('deleteList', { listId: this.id })
|
||||||
this.$router.push({ name: 'lists' })
|
this.$router.push({ name: 'lists' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="panel-default panel list-edit">
|
<div class="panel-default panel ListEdit">
|
||||||
<div
|
<div
|
||||||
ref="header"
|
ref="header"
|
||||||
class="panel-heading"
|
class="panel-heading list-edit-heading"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="button-unstyled go-back-button"
|
class="button-unstyled go-back-button"
|
||||||
|
@ -13,54 +13,151 @@
|
||||||
icon="chevron-left"
|
icon="chevron-left"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<div class="title">
|
||||||
<div class="input-wrap">
|
<i18n-t
|
||||||
<input
|
v-if="id"
|
||||||
ref="title"
|
keypath="lists.editing_list"
|
||||||
v-model="title"
|
>
|
||||||
:placeholder="$t('lists.title')"
|
<template #listTitle>
|
||||||
>
|
{{ title }}
|
||||||
</div>
|
</template>
|
||||||
<div class="member-list">
|
</i18n-t>
|
||||||
<div
|
<i18n-t
|
||||||
v-for="user in selectedUsers"
|
v-else
|
||||||
:key="user.id"
|
keypath="lists.creating_list"
|
||||||
class="member"
|
|
||||||
>
|
|
||||||
<BasicUserCard
|
|
||||||
:user="user"
|
|
||||||
:class="isSelected(user) ? 'selected' : ''"
|
|
||||||
@click.capture.prevent="selectUser(user)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ListsUserSearch @results="onResults" />
|
<div class="panel-body">
|
||||||
<div class="member-list">
|
<div class="input-wrap">
|
||||||
<div
|
<label for="list-edit-title">{{ $t('lists.title') }}</label>
|
||||||
v-for="user in users"
|
{{ ' ' }}
|
||||||
:key="user.id"
|
<input
|
||||||
class="member"
|
id="list-edit-title"
|
||||||
>
|
ref="title"
|
||||||
<BasicUserCard
|
v-model="titleDraft"
|
||||||
:user="user"
|
>
|
||||||
:class="isSelected(user) ? 'selected' : ''"
|
<button
|
||||||
@click.capture.prevent="selectUser(user)"
|
v-if="id"
|
||||||
/>
|
class="btn button-default follow-button"
|
||||||
|
@click="updateListTitle"
|
||||||
|
>
|
||||||
|
{{ $t('lists.update_title') }}
|
||||||
|
</button>
|
||||||
</div>
|
</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>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -69,28 +166,43 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.list-edit {
|
.ListEdit {
|
||||||
.input-wrap {
|
--panel-body-padding: 0.5em;
|
||||||
display: flex;
|
|
||||||
margin: 0.7em 0.5em 0.7em 0.5em;
|
|
||||||
|
|
||||||
input {
|
height: calc(100vh - var(--navbar-height));
|
||||||
width: 100%;
|
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 {
|
.search-icon {
|
||||||
margin-right: 0.3em;
|
margin-right: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-list {
|
.users-list {
|
||||||
padding-bottom: 0.7rem;
|
padding-bottom: 0.7rem;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.basic-user-card:hover,
|
& .search-list,
|
||||||
.basic-user-card.selected {
|
& .members-list {
|
||||||
cursor: pointer;
|
overflow: hidden;
|
||||||
background-color: var(--selectedPost, $fallback--lightBg);
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.go-back-button {
|
.go-back-button {
|
||||||
|
@ -102,7 +214,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
margin: 0.5em;
|
margin: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-footer {
|
||||||
|
grid-template-columns: minmax(10%, 1fr);
|
||||||
|
|
||||||
|
.footer-button {
|
||||||
|
min-width: 9em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,28 +1,17 @@
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||||
import {
|
import { getListEntries } from 'src/components/navigation/filter.js'
|
||||||
faUsers,
|
|
||||||
faGlobe,
|
|
||||||
faBookmark,
|
|
||||||
faEnvelope,
|
|
||||||
faHome
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
library.add(
|
export const ListsMenuContent = {
|
||||||
faUsers,
|
props: [
|
||||||
faGlobe,
|
'showPin'
|
||||||
faBookmark,
|
],
|
||||||
faEnvelope,
|
components: {
|
||||||
faHome
|
NavigationEntry
|
||||||
)
|
|
||||||
|
|
||||||
const ListsMenuContent = {
|
|
||||||
created () {
|
|
||||||
this.$store.dispatch('startFetchingLists')
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
lists: state => state.lists.allLists,
|
lists: getListEntries,
|
||||||
currentUser: state => state.users.currentUser,
|
currentUser: state => state.users.currentUser,
|
||||||
privateMode: state => state.instance.private,
|
privateMode: state => state.instance.private,
|
||||||
federating: state => state.instance.federating
|
federating: state => state.instance.federating
|
||||||
|
|
|
@ -1,16 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<NavigationEntry
|
||||||
v-for="list in lists.slice().reverse()"
|
v-for="item in lists"
|
||||||
:key="list.id"
|
:key="item.name"
|
||||||
>
|
:show-pin="showPin"
|
||||||
<router-link
|
:item="item"
|
||||||
class="menu-item"
|
/>
|
||||||
:to="{ name: 'lists-timeline', params: { id: list.id } }"
|
|
||||||
>
|
|
||||||
{{ list.title }}
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</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.listId = route.params.id
|
||||||
this.$store.dispatch('stopFetchingTimeline', 'list')
|
this.$store.dispatch('stopFetchingTimeline', 'list')
|
||||||
this.$store.commit('clearTimeline', { timeline: '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 })
|
this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
this.listId = this.$route.params.id
|
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 })
|
this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId })
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted () {
|
||||||
|
|
|
@ -15,6 +15,7 @@ const ListsUserSearch = {
|
||||||
components: {
|
components: {
|
||||||
Checkbox
|
Checkbox
|
||||||
},
|
},
|
||||||
|
emits: ['loading', 'loadingDone', 'results'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -33,12 +34,16 @@ const ListsUserSearch = {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
this.$emit('loading')
|
||||||
this.userIds = []
|
this.userIds = []
|
||||||
this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts', following: this.followingOnly })
|
this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts', following: this.followingOnly })
|
||||||
.then(data => {
|
.then(data => {
|
||||||
this.loading = false
|
|
||||||
this.$emit('results', data.accounts.map(a => a.id))
|
this.$emit('results', data.accounts.map(a => a.id))
|
||||||
})
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
this.$emit('loadingDone')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="ListsUserSearch">
|
||||||
<div class="input-wrap">
|
<div class="input-wrap">
|
||||||
<div class="input-search">
|
<div class="input-search">
|
||||||
<FAIcon
|
<FAIcon
|
||||||
|
@ -29,17 +29,19 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.input-wrap {
|
.ListsUserSearch {
|
||||||
display: flex;
|
.input-wrap {
|
||||||
margin: 0.7em 0.5em 0.7em 0.5em;
|
display: flex;
|
||||||
|
margin: 0.7em 0.5em 0.7em 0.5em;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
margin-right: 0.3em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-icon {
|
|
||||||
margin-right: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
|
||||||
import { mapGetters, mapState } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
|
@ -16,6 +17,7 @@ const MentionLink = {
|
||||||
name: 'MentionLink',
|
name: 'MentionLink',
|
||||||
components: {
|
components: {
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
|
UnicodeDomainIndicator,
|
||||||
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -47,6 +47,9 @@
|
||||||
class="serverName"
|
class="serverName"
|
||||||
:class="{ '-faded': shouldFadeDomain }"
|
:class="{ '-faded': shouldFadeDomain }"
|
||||||
v-html="'@' + serverName"
|
v-html="'@' + serverName"
|
||||||
|
/><UnicodeDomainIndicator
|
||||||
|
v-if="shouldShowFullUserName"
|
||||||
|
:user="user"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
|
|
@ -2,6 +2,7 @@ import SideDrawer from '../side_drawer/side_drawer.vue'
|
||||||
import Notifications from '../notifications/notifications.vue'
|
import Notifications from '../notifications/notifications.vue'
|
||||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||||
import GestureService from '../../services/gesture_service/gesture_service'
|
import GestureService from '../../services/gesture_service/gesture_service'
|
||||||
|
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
|
@ -19,7 +20,8 @@ library.add(
|
||||||
const MobileNav = {
|
const MobileNav = {
|
||||||
components: {
|
components: {
|
||||||
SideDrawer,
|
SideDrawer,
|
||||||
Notifications
|
Notifications,
|
||||||
|
NavigationPins
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
notificationsCloseGesture: undefined,
|
notificationsCloseGesture: undefined,
|
||||||
|
@ -47,7 +49,10 @@ const MobileNav = {
|
||||||
isChat () {
|
isChat () {
|
||||||
return this.$route.name === 'chat'
|
return this.$route.name === 'chat'
|
||||||
},
|
},
|
||||||
...mapGetters(['unreadChatCount'])
|
...mapGetters(['unreadChatCount']),
|
||||||
|
chatsPinned () {
|
||||||
|
return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleMobileSidebar () {
|
toggleMobileSidebar () {
|
||||||
|
|
|
@ -17,20 +17,12 @@
|
||||||
icon="bars"
|
icon="bars"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="unreadChatCount"
|
v-if="unreadChatCount && !chatsPinned"
|
||||||
class="alert-dot"
|
class="alert-dot"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<router-link
|
<NavigationPins class="pins" />
|
||||||
v-if="!hideSitename"
|
</div> <div class="item right">
|
||||||
class="site-name"
|
|
||||||
:to="{ name: 'root' }"
|
|
||||||
active-class="home"
|
|
||||||
>
|
|
||||||
{{ sitename }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div class="item right">
|
|
||||||
<button
|
<button
|
||||||
v-if="currentUser"
|
v-if="currentUser"
|
||||||
class="button-unstyled mobile-nav-button"
|
class="button-unstyled mobile-nav-button"
|
||||||
|
@ -94,6 +86,7 @@
|
||||||
grid-template-columns: 2fr auto;
|
grid-template-columns: 2fr auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--topBarLink, $fallback--link);
|
color: var(--topBarLink, $fallback--link);
|
||||||
}
|
}
|
||||||
|
@ -178,13 +171,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pins {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.pinned-item {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-notifications {
|
.mobile-notifications {
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: calc(100vh - var(--navbar-height));
|
height: calc(100vh - var(--navbar-height));
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--text, $fallback--text);
|
color: var(--text, $fallback--text);
|
||||||
background-color: $fallback--bg;
|
background-color: $fallback--bg;
|
||||||
|
@ -194,14 +194,17 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
.panel:after {
|
|
||||||
|
.panel::after {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel .panel-heading {
|
.panel .panel-heading {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
|
@ -10,7 +10,8 @@ library.add(
|
||||||
|
|
||||||
const HIDDEN_FOR_PAGES = new Set([
|
const HIDDEN_FOR_PAGES = new Set([
|
||||||
'chats',
|
'chats',
|
||||||
'chat'
|
'chat',
|
||||||
|
'lists-edit'
|
||||||
])
|
])
|
||||||
|
|
||||||
const MobilePostStatusButton = {
|
const MobilePostStatusButton = {
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import TimelineMenuContent from '../timeline_menu/timeline_menu_content.vue'
|
import ListsMenuContent from 'src/components/lists_menu/lists_menu_content.vue'
|
||||||
import ListsMenuContent from '../lists_menu/lists_menu_content.vue'
|
|
||||||
import { mapState, mapGetters } from 'vuex'
|
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 { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
|
@ -30,21 +34,23 @@ library.add(
|
||||||
faStream,
|
faStream,
|
||||||
faList
|
faList
|
||||||
)
|
)
|
||||||
|
|
||||||
const NavPanel = {
|
const NavPanel = {
|
||||||
|
props: ['forceExpand', 'forceEditMode'],
|
||||||
created () {
|
created () {
|
||||||
if (this.currentUser && this.currentUser.locked) {
|
|
||||||
this.$store.dispatch('startFetchingFollowRequests')
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
TimelineMenuContent,
|
ListsMenuContent,
|
||||||
ListsMenuContent
|
NavigationEntry,
|
||||||
|
NavigationPins,
|
||||||
|
Checkbox
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
editMode: false,
|
||||||
showTimelines: 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: {
|
methods: {
|
||||||
|
@ -53,19 +59,62 @@ const NavPanel = {
|
||||||
},
|
},
|
||||||
toggleLists () {
|
toggleLists () {
|
||||||
this.showLists = !this.showLists
|
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: {
|
computed: {
|
||||||
listsNavigation () {
|
|
||||||
return this.$store.getters.mergedConfig.listsNavigation
|
|
||||||
},
|
|
||||||
...mapState({
|
...mapState({
|
||||||
currentUser: state => state.users.currentUser,
|
currentUser: state => state.users.currentUser,
|
||||||
followRequestCount: state => state.api.followRequests.length,
|
followRequestCount: state => state.api.followRequests.length,
|
||||||
privateMode: state => state.instance.private,
|
privateMode: state => state.instance.private,
|
||||||
federating: state => state.instance.federating,
|
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'])
|
...mapGetters(['unreadChatCount'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,135 +1,99 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="NavPanel">
|
<div class="NavPanel">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<ul>
|
<div
|
||||||
<li v-if="currentUser || !privateMode">
|
v-if="!forceExpand"
|
||||||
<button
|
class="panel-heading nav-panel-heading"
|
||||||
class="button-unstyled menu-item"
|
>
|
||||||
@click="toggleTimelines"
|
<NavigationPins :limit="6" />
|
||||||
>
|
<div class="spacer" />
|
||||||
<FAIcon
|
<button
|
||||||
fixed-width
|
class="button-unstyled"
|
||||||
class="fa-scale-110"
|
@click="toggleCollapse"
|
||||||
icon="stream"
|
>
|
||||||
/>{{ $t("nav.timelines") }}
|
<FAIcon
|
||||||
<FAIcon
|
class="timelines-chevron"
|
||||||
class="timelines-chevron"
|
fixed-width
|
||||||
fixed-width
|
:icon="collapsed ? 'chevron-down' : 'chevron-up'"
|
||||||
:icon="showTimelines ? 'chevron-up' : 'chevron-down'"
|
/>
|
||||||
|
</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>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
<li v-if="currentUser && listsNavigation">
|
<NavigationEntry
|
||||||
<button
|
v-if="currentUser"
|
||||||
class="button-unstyled menu-item"
|
:show-pin="false"
|
||||||
@click="toggleLists"
|
:item="{ icon: 'list', label: 'nav.lists' }"
|
||||||
>
|
:aria-expanded="showLists ? 'true' : 'false'"
|
||||||
<router-link
|
@click="toggleLists"
|
||||||
: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">
|
|
||||||
<router-link
|
<router-link
|
||||||
|
:title="$t('lists.manage_lists')"
|
||||||
|
class="extra-button"
|
||||||
:to="{ name: 'lists' }"
|
:to="{ name: 'lists' }"
|
||||||
@click.stop
|
@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
|
<FAIcon
|
||||||
|
class="extra-button"
|
||||||
fixed-width
|
fixed-width
|
||||||
class="fa-scale-110"
|
icon="wrench"
|
||||||
icon="bell"
|
/>
|
||||||
/>{{ $t("nav.interactions") }}
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
<FAIcon
|
||||||
<li v-if="currentUser && pleromaChatMessagesAvailable">
|
class="timelines-chevron"
|
||||||
<router-link
|
fixed-width
|
||||||
class="menu-item"
|
:icon="showLists ? 'chevron-up' : 'chevron-down'"
|
||||||
:to="{ name: 'chats', params: { username: currentUser.screen_name } }"
|
/>
|
||||||
>
|
</NavigationEntry>
|
||||||
<div
|
<div
|
||||||
v-if="unreadChatCount"
|
v-show="showLists"
|
||||||
class="badge badge-notification"
|
class="timelines-background"
|
||||||
>
|
>
|
||||||
{{ unreadChatCount }}
|
<ListsMenuContent
|
||||||
</div>
|
:show-pin="editMode || forceEditMode"
|
||||||
<FAIcon
|
class="timelines"
|
||||||
fixed-width
|
/>
|
||||||
class="fa-scale-110"
|
</div>
|
||||||
icon="comments"
|
<NavigationEntry
|
||||||
/>{{ $t("nav.chats") }}
|
v-for="item in rootItems"
|
||||||
</router-link>
|
:key="item.name"
|
||||||
</li>
|
:show-pin="editMode || forceEditMode"
|
||||||
<li v-if="currentUser && currentUser.locked">
|
:item="item"
|
||||||
<router-link
|
/>
|
||||||
class="menu-item"
|
<NavigationEntry
|
||||||
:to="{ name: 'friend-requests' }"
|
v-if="!forceEditMode && currentUser"
|
||||||
>
|
:show-pin="false"
|
||||||
<FAIcon
|
:item="{ label: editMode ? $t('nav.edit_finish') : $t('nav.edit_pinned'), icon: editMode ? 'check' : 'wrench' }"
|
||||||
fixed-width
|
@click="toggleEditMode"
|
||||||
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>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -180,54 +144,23 @@
|
||||||
border: none;
|
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 {
|
.timelines-chevron {
|
||||||
margin-left: 0.8em;
|
margin-left: 0.8em;
|
||||||
|
margin-right: 0.8em;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
.timelines-chevron {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.timelines-background {
|
.timelines-background {
|
||||||
padding: 0 0 0 0.6em;
|
padding: 0 0 0 0.6em;
|
||||||
background-color: $fallback--lightBg;
|
background-color: $fallback--lightBg;
|
||||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||||
border-top: 1px solid;
|
border-bottom: 1px solid;
|
||||||
border-color: $fallback--border;
|
border-color: $fallback--border;
|
||||||
border-color: var(--border, $fallback--border);
|
border-color: var(--border, $fallback--border);
|
||||||
}
|
}
|
||||||
|
@ -237,14 +170,9 @@
|
||||||
background-color: var(--bg, $fallback--bg);
|
background-color: var(--bg, $fallback--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-scale-110 {
|
.nav-panel-heading {
|
||||||
margin-right: 0.8em;
|
// breaks without a unit
|
||||||
}
|
--panel-heading-height-padding: 0em;
|
||||||
|
|
||||||
.badge {
|
|
||||||
position: absolute;
|
|
||||||
right: 0.6rem;
|
|
||||||
top: 1.25em;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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]
|
||||||
|
}))
|
|
@ -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'
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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>
|
|
@ -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
|
|
@ -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 UserCard from '../user_card/user_card.vue'
|
||||||
import Timeago from '../timeago/timeago.vue'
|
import Timeago from '../timeago/timeago.vue'
|
||||||
import Report from '../report/report.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 RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
import UserPopover from '../user_popover/user_popover.vue'
|
import UserPopover from '../user_popover/user_popover.vue'
|
||||||
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
|
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
|
||||||
|
@ -50,7 +51,8 @@ const Notification = {
|
||||||
Status,
|
Status,
|
||||||
Report,
|
Report,
|
||||||
RichContent,
|
RichContent,
|
||||||
UserPopover
|
UserPopover,
|
||||||
|
UserLink
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleUserExpanded () {
|
toggleUserExpanded () {
|
||||||
|
|
|
@ -11,9 +11,10 @@
|
||||||
class="Notification container -muted"
|
class="Notification container -muted"
|
||||||
>
|
>
|
||||||
<small>
|
<small>
|
||||||
<router-link :to="userProfileLink">
|
<user-link
|
||||||
{{ notification.from_profile.screen_name_ui }}
|
:user="notification.from_profile"
|
||||||
</router-link>
|
:at="false"
|
||||||
|
/>
|
||||||
</small>
|
</small>
|
||||||
<button
|
<button
|
||||||
class="button-unstyled unmute"
|
class="button-unstyled unmute"
|
||||||
|
@ -174,12 +175,10 @@
|
||||||
v-if="notification.type === 'follow' || notification.type === 'follow_request'"
|
v-if="notification.type === 'follow' || notification.type === 'follow_request'"
|
||||||
class="follow-text"
|
class="follow-text"
|
||||||
>
|
>
|
||||||
<router-link
|
<user-link
|
||||||
:to="userProfileLink"
|
|
||||||
class="follow-name"
|
class="follow-name"
|
||||||
>
|
:user="notification.from_profile"
|
||||||
@{{ notification.from_profile.screen_name_ui }}
|
/>
|
||||||
</router-link>
|
|
||||||
<div
|
<div
|
||||||
v-if="notification.type === 'follow_request'"
|
v-if="notification.type === 'follow_request'"
|
||||||
style="white-space: nowrap;"
|
style="white-space: nowrap;"
|
||||||
|
@ -210,9 +209,9 @@
|
||||||
v-else-if="notification.type === 'move'"
|
v-else-if="notification.type === 'move'"
|
||||||
class="move-text"
|
class="move-text"
|
||||||
>
|
>
|
||||||
<router-link :to="targetUserProfileLink">
|
<user-link
|
||||||
@{{ notification.target.screen_name_ui }}
|
:user="notification.target"
|
||||||
</router-link>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Report
|
<Report
|
||||||
v-else-if="notification.type === 'pleroma:report'"
|
v-else-if="notification.type === 'pleroma:report'"
|
||||||
|
|
|
@ -4,7 +4,7 @@ const Popover = {
|
||||||
// Action to trigger popover: either 'hover' or 'click'
|
// Action to trigger popover: either 'hover' or 'click'
|
||||||
trigger: String,
|
trigger: String,
|
||||||
|
|
||||||
// Either 'top' or 'bottom'
|
// 'top', 'bottom', 'left', 'right'
|
||||||
placement: String,
|
placement: String,
|
||||||
|
|
||||||
// Takes object with properties 'x' and 'y', values of these can be
|
// Takes object with properties 'x' and 'y', values of these can be
|
||||||
|
@ -84,6 +84,8 @@ const Popover = {
|
||||||
const anchorStyle = getComputedStyle(anchorEl)
|
const anchorStyle = getComputedStyle(anchorEl)
|
||||||
const topPadding = parseFloat(anchorStyle.paddingTop)
|
const topPadding = parseFloat(anchorStyle.paddingTop)
|
||||||
const bottomPadding = parseFloat(anchorStyle.paddingBottom)
|
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
|
// Screen position of the origin point for popover = center of the anchor
|
||||||
const origin = {
|
const origin = {
|
||||||
|
@ -170,7 +172,7 @@ const Popover = {
|
||||||
if (overlayCenter) {
|
if (overlayCenter) {
|
||||||
translateX = origin.x + horizOffset
|
translateX = origin.x + horizOffset
|
||||||
translateY = origin.y + vertOffset
|
translateY = origin.y + vertOffset
|
||||||
} else {
|
} else if (this.placement !== 'right' && this.placement !== 'left') {
|
||||||
// Default to whatever user wished with placement prop
|
// Default to whatever user wished with placement prop
|
||||||
let usingTop = this.placement !== 'bottom'
|
let usingTop = this.placement !== 'bottom'
|
||||||
|
|
||||||
|
@ -189,6 +191,25 @@ const Popover = {
|
||||||
|
|
||||||
const xOffset = (this.offset && this.offset.x) || 0
|
const xOffset = (this.offset && this.offset.x) || 0
|
||||||
translateX = origin.x + horizOffset + xOffset
|
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 = {
|
this.styles = {
|
||||||
|
|
|
@ -126,6 +126,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.-has-submenu {
|
||||||
|
.chevron-icon {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:active, &:hover {
|
&:active, &:hover {
|
||||||
background-color: $fallback--lightBg;
|
background-color: $fallback--lightBg;
|
||||||
background-color: var(--selectedMenuPopover, $fallback--lightBg);
|
background-color: var(--selectedMenuPopover, $fallback--lightBg);
|
||||||
|
|
|
@ -47,6 +47,8 @@
|
||||||
class="cancel-icon fa-scale-110 fa-old-padding"
|
class="cancel-icon fa-scale-110 fa-old-padding"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
<span class="spacer" />
|
||||||
|
<span class="spacer" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -101,11 +101,6 @@
|
||||||
{{ $t('settings.hide_shoutbox') }}
|
{{ $t('settings.hide_shoutbox') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<BooleanSetting path="listsNavigation">
|
|
||||||
{{ $t('settings.lists_navigation') }}
|
|
||||||
</BooleanSetting>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<h3>{{ $t('settings.columns') }}</h3>
|
<h3>{{ $t('settings.columns') }}</h3>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { mapState, mapGetters } from 'vuex'
|
||||||
import UserCard from '../user_card/user_card.vue'
|
import UserCard from '../user_card/user_card.vue'
|
||||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||||
import GestureService from '../../services/gesture_service/gesture_service'
|
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 { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faSignInAlt,
|
faSignInAlt,
|
||||||
|
@ -15,6 +16,7 @@ import {
|
||||||
faTachometerAlt,
|
faTachometerAlt,
|
||||||
faCog,
|
faCog,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
|
faCompass,
|
||||||
faList
|
faList
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
@ -30,6 +32,7 @@ library.add(
|
||||||
faTachometerAlt,
|
faTachometerAlt,
|
||||||
faCog,
|
faCog,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
|
faCompass,
|
||||||
faList
|
faList
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -80,10 +83,16 @@ const SideDrawer = {
|
||||||
return this.$store.state.instance.federating
|
return this.$store.state.instance.federating
|
||||||
},
|
},
|
||||||
timelinesRoute () {
|
timelinesRoute () {
|
||||||
|
let name
|
||||||
if (this.$store.state.interface.lastTimeline) {
|
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({
|
...mapState({
|
||||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
v-if="currentUser || !privateMode"
|
v-if="currentUser || !privateMode"
|
||||||
@click="toggleDrawer"
|
@click="toggleDrawer"
|
||||||
>
|
>
|
||||||
<router-link :to="{ name: timelinesRoute }">
|
<router-link :to="timelinesRoute">
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
class="fa-scale-110 fa-old-padding"
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
@ -191,6 +191,18 @@
|
||||||
/> {{ $t("nav.administration") }}
|
/> {{ $t("nav.administration") }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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
|
<li
|
||||||
v-if="currentUser"
|
v-if="currentUser"
|
||||||
@click="toggleDrawer"
|
@click="toggleDrawer"
|
||||||
|
|
|
@ -13,6 +13,7 @@ import StatusPopover from '../status_popover/status_popover.vue'
|
||||||
import UserPopover from '../user_popover/user_popover.vue'
|
import UserPopover from '../user_popover/user_popover.vue'
|
||||||
import UserListPopover from '../user_list_popover/user_list_popover.vue'
|
import UserListPopover from '../user_list_popover/user_list_popover.vue'
|
||||||
import EmojiReactions from '../emoji_reactions/emoji_reactions.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 MentionsLine from 'src/components/mentions_line/mentions_line.vue'
|
||||||
import MentionLink from 'src/components/mention_link/mention_link.vue'
|
import MentionLink from 'src/components/mention_link/mention_link.vue'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
@ -115,7 +116,8 @@ const Status = {
|
||||||
RichContent,
|
RichContent,
|
||||||
MentionLink,
|
MentionLink,
|
||||||
MentionsLine,
|
MentionsLine,
|
||||||
UserPopover
|
UserPopover,
|
||||||
|
UserLink
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
'statusoid',
|
'statusoid',
|
||||||
|
|
|
@ -25,9 +25,10 @@
|
||||||
class="fa-scale-110 fa-old-padding repeat-icon"
|
class="fa-scale-110 fa-old-padding repeat-icon"
|
||||||
icon="retweet"
|
icon="retweet"
|
||||||
/>
|
/>
|
||||||
<router-link :to="userProfileLink">
|
<user-link
|
||||||
{{ status.user.screen_name_ui }}
|
:user="status.user"
|
||||||
</router-link>
|
:at="false"
|
||||||
|
/>
|
||||||
</small>
|
</small>
|
||||||
<small
|
<small
|
||||||
v-if="showReasonMutedThread"
|
v-if="showReasonMutedThread"
|
||||||
|
@ -164,13 +165,12 @@
|
||||||
>
|
>
|
||||||
{{ status.user.name }}
|
{{ status.user.name }}
|
||||||
</h4>
|
</h4>
|
||||||
<router-link
|
<user-link
|
||||||
class="account-name"
|
class="account-name"
|
||||||
:title="status.user.screen_name_ui"
|
:title="status.user.screen_name_ui"
|
||||||
:to="userProfileLink"
|
:user="status.user"
|
||||||
>
|
:at="false"
|
||||||
{{ status.user.screen_name_ui }}
|
/>
|
||||||
</router-link>
|
|
||||||
<img
|
<img
|
||||||
v-if="!!(status.user && status.user.favicon)"
|
v-if="!!(status.user && status.user.favicon)"
|
||||||
class="status-favicon"
|
class="status-favicon"
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
&::after, &::before {
|
&::after, &::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="['Timeline', classes.root]">
|
<div :class="['Timeline', classes.root]">
|
||||||
<div :class="classes.header">
|
<div :class="classes.header">
|
||||||
<TimelineMenu v-if="!embedded" />
|
<TimelineMenu
|
||||||
|
v-if="!embedded"
|
||||||
|
:timeline-name="timelineName"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="showScrollTop"
|
v-if="showScrollTop"
|
||||||
class="button-unstyled scroll-to-top-button"
|
class="button-unstyled scroll-to-top-button"
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import Popover from '../popover/popover.vue'
|
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 { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { TIMELINES } from 'src/components/navigation/navigation.js'
|
||||||
import {
|
import {
|
||||||
faChevronDown
|
faChevronDown
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
@ -22,11 +24,13 @@ export const timelineNames = () => {
|
||||||
const TimelineMenu = {
|
const TimelineMenu = {
|
||||||
components: {
|
components: {
|
||||||
Popover,
|
Popover,
|
||||||
TimelineMenuContent
|
NavigationEntry,
|
||||||
|
ListsMenuContent
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isOpen: false
|
isOpen: false,
|
||||||
|
timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k }))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
@ -34,6 +38,12 @@ const TimelineMenu = {
|
||||||
this.$store.dispatch('setLastTimeline', this.$route.name)
|
this.$store.dispatch('setLastTimeline', this.$route.name)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
useListsMenu () {
|
||||||
|
const route = this.$route.name
|
||||||
|
return route === 'lists-timeline'
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openMenu () {
|
openMenu () {
|
||||||
// $nextTick is too fast, animation won't play back but
|
// $nextTick is too fast, animation won't play back but
|
||||||
|
|
|
@ -10,7 +10,19 @@
|
||||||
@close="() => isOpen = false"
|
@close="() => isOpen = false"
|
||||||
>
|
>
|
||||||
<template #content>
|
<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>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<span class="button-unstyled title timeline-menu-title">
|
<span class="button-unstyled title timeline-menu-title">
|
||||||
|
@ -138,8 +150,7 @@
|
||||||
background-color: $fallback--lightBg;
|
background-color: $fallback--lightBg;
|
||||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--selectedMenuText, $fallback--text);
|
color: var(--selectedMenuText, $fallback--text); --faint: var(--selectedMenuFaintText, $fallback--faint);
|
||||||
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
|
||||||
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
||||||
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||||
--icon: var(--selectedMenuIcon, $fallback--icon);
|
--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 &&
|
return !this.$store.state.instance.disableUpdateNotification &&
|
||||||
this.$store.state.users.currentUser &&
|
this.$store.state.users.currentUser &&
|
||||||
this.$store.state.serverSideStorage.flagStorage.updateCounter < CURRENT_UPDATE_COUNTER &&
|
this.$store.state.serverSideStorage.flagStorage.updateCounter < CURRENT_UPDATE_COUNTER &&
|
||||||
!this.$store.state.serverSideStorage.flagStorage.dontShowUpdateNotifs
|
!this.$store.state.serverSideStorage.prefsStorage.simple.dontShowUpdateNotifs
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -48,7 +48,7 @@ const UpdateNotification = {
|
||||||
neverShowAgain () {
|
neverShowAgain () {
|
||||||
this.toggleShow()
|
this.toggleShow()
|
||||||
this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER })
|
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')
|
this.$store.dispatch('pushServerSideStorage')
|
||||||
},
|
},
|
||||||
dismiss () {
|
dismiss () {
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
<template #linkToArtist>
|
<template #linkToArtist>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://post.ebin.club/pipivovott"
|
href="https://post.ebin.club/users/pipivovott"
|
||||||
>pipivovott</a>
|
>pipivovott</a>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import FollowButton from '../follow_button/follow_button.vue'
|
||||||
import ModerationTools from '../moderation_tools/moderation_tools.vue'
|
import ModerationTools from '../moderation_tools/moderation_tools.vue'
|
||||||
import AccountActions from '../account_actions/account_actions.vue'
|
import AccountActions from '../account_actions/account_actions.vue'
|
||||||
import Select from '../select/select.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 RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
@ -134,7 +135,8 @@ export default {
|
||||||
ProgressButton,
|
ProgressButton,
|
||||||
FollowButton,
|
FollowButton,
|
||||||
Select,
|
Select,
|
||||||
RichContent
|
RichContent,
|
||||||
|
UserLink
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
muteUser () {
|
muteUser () {
|
||||||
|
|
|
@ -106,13 +106,10 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="bottom-line">
|
<div class="bottom-line">
|
||||||
<router-link
|
<user-link
|
||||||
class="user-screen-name"
|
class="user-screen-name"
|
||||||
:title="user.screen_name_ui"
|
:user="user"
|
||||||
:to="userProfileLink(user)"
|
/>
|
||||||
>
|
|
||||||
@{{ user.screen_name_ui }}
|
|
||||||
</router-link>
|
|
||||||
<template v-if="!hideBio">
|
<template v-if="!hideBio">
|
||||||
<span
|
<span
|
||||||
v-if="user.deactivated"
|
v-if="user.deactivated"
|
||||||
|
|
|
@ -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>
|
|
@ -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
|
|
@ -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 { defineAsyncComponent } from 'vue'
|
||||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
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 { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
@ -15,6 +16,7 @@ const UserListPopover = {
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
RichContent,
|
RichContent,
|
||||||
|
UnicodeDomainIndicator,
|
||||||
Popover: defineAsyncComponent(() => import('../popover/popover.vue')),
|
Popover: defineAsyncComponent(() => import('../popover/popover.vue')),
|
||||||
UserAvatar: defineAsyncComponent(() => import('../user_avatar/user_avatar.vue'))
|
UserAvatar: defineAsyncComponent(() => import('../user_avatar/user_avatar.vue'))
|
||||||
},
|
},
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
:emoji="user.emoji"
|
:emoji="user.emoji"
|
||||||
/>
|
/>
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
<!-- 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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -2,13 +2,15 @@ import Status from '../status/status.vue'
|
||||||
import List from '../list/list.vue'
|
import List from '../list/list.vue'
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
import Modal from '../modal/modal.vue'
|
import Modal from '../modal/modal.vue'
|
||||||
|
import UserLink from '../user_link/user_link.vue'
|
||||||
|
|
||||||
const UserReportingModal = {
|
const UserReportingModal = {
|
||||||
components: {
|
components: {
|
||||||
Status,
|
Status,
|
||||||
List,
|
List,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Modal
|
Modal,
|
||||||
|
UserLink
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -5,9 +5,13 @@
|
||||||
>
|
>
|
||||||
<div class="user-reporting-panel panel">
|
<div class="user-reporting-panel panel">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<div class="title">
|
<i18n-t
|
||||||
{{ $t('user_reporting.title', [user.screen_name_ui]) }}
|
tag="div"
|
||||||
</div>
|
keypath="user_reporting.title"
|
||||||
|
class="title"
|
||||||
|
>
|
||||||
|
<UserLink :user="user" />
|
||||||
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="user-reporting-panel-left">
|
<div class="user-reporting-panel-left">
|
||||||
|
|
|
@ -80,11 +80,16 @@
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"verify": "Verify",
|
"verify": "Verify",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
|
"undo": "Undo",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
"peek": "Peek",
|
"peek": "Peek",
|
||||||
"role": {
|
"role": {
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"moderator": "Moderator"
|
"moderator": "Moderator"
|
||||||
},
|
},
|
||||||
|
"unpin": "Unpin item",
|
||||||
|
"pin": "Pin item",
|
||||||
"flash_content": "Click to show Flash content using Ruffle (Experimental, may not work).",
|
"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_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.",
|
"flash_fail": "Failed to load flash content, see console for details.",
|
||||||
|
@ -149,7 +154,10 @@
|
||||||
"preferences": "Preferences",
|
"preferences": "Preferences",
|
||||||
"timelines": "Timelines",
|
"timelines": "Timelines",
|
||||||
"chats": "Chats",
|
"chats": "Chats",
|
||||||
"lists": "Lists"
|
"lists": "Lists",
|
||||||
|
"edit_nav_mobile": "Customize navigation bar",
|
||||||
|
"edit_pinned": "Edit pinned items",
|
||||||
|
"edit_finish": "Done editing"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"broken_favorite": "Unknown status, searching for it…",
|
"broken_favorite": "Unknown status, searching for it…",
|
||||||
|
@ -987,7 +995,18 @@
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"save": "Save changes",
|
"save": "Save changes",
|
||||||
"delete": "Delete list",
|
"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": {
|
"file_type": {
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
|
@ -1006,5 +1025,8 @@
|
||||||
"update_changelog": "For more details on what's changed, see {theFullChangelog}.",
|
"update_changelog": "For more details on what's changed, see {theFullChangelog}.",
|
||||||
"update_changelog_here": "the full changelog",
|
"update_changelog_here": "the full changelog",
|
||||||
"art_by": "Art by {linkToArtist}"
|
"art_by": "Art by {linkToArtist}"
|
||||||
|
},
|
||||||
|
"unicode_domain_indicator": {
|
||||||
|
"tooltip": "This domain contains non-ascii characters."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,9 @@ const api = {
|
||||||
mastoUserSocketStatus: null,
|
mastoUserSocketStatus: null,
|
||||||
followRequests: []
|
followRequests: []
|
||||||
},
|
},
|
||||||
|
getters: {
|
||||||
|
followRequestCount: state => state.api.followRequests.length
|
||||||
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setBackendInteractor (state, backendInteractor) {
|
setBackendInteractor (state, backendInteractor) {
|
||||||
state.backendInteractor = backendInteractor
|
state.backendInteractor = backendInteractor
|
||||||
|
|
|
@ -89,7 +89,6 @@ export const defaultState = {
|
||||||
contentColumnWidth: '45rem',
|
contentColumnWidth: '45rem',
|
||||||
notifsColumnWidth: '25rem',
|
notifsColumnWidth: '25rem',
|
||||||
navbarColumnStretch: false,
|
navbarColumnStretch: false,
|
||||||
listsNavigation: false,
|
|
||||||
greentext: undefined, // instance default
|
greentext: undefined, // instance default
|
||||||
useAtIcon: undefined, // instance default
|
useAtIcon: undefined, // instance default
|
||||||
mentionLinkDisplay: undefined, // instance default
|
mentionLinkDisplay: undefined, // instance default
|
||||||
|
|
|
@ -9,27 +9,43 @@ export const mutations = {
|
||||||
setLists (state, value) {
|
setLists (state, value) {
|
||||||
state.allLists = value
|
state.allLists = value
|
||||||
},
|
},
|
||||||
setList (state, { id, title }) {
|
setList (state, { listId, title }) {
|
||||||
if (!state.allListsObject[id]) {
|
if (!state.allListsObject[listId]) {
|
||||||
state.allListsObject[id] = {}
|
state.allListsObject[listId] = { accountIds: [] }
|
||||||
}
|
}
|
||||||
state.allListsObject[id].title = title
|
state.allListsObject[listId].title = title
|
||||||
|
|
||||||
if (!find(state.allLists, { id })) {
|
const entry = find(state.allLists, { id: listId })
|
||||||
state.allLists.push({ id, title })
|
if (!entry) {
|
||||||
|
state.allLists.push({ id: listId, title })
|
||||||
} else {
|
} else {
|
||||||
find(state.allLists, { id }).title = title
|
entry.title = title
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setListAccounts (state, { id, accountIds }) {
|
setListAccounts (state, { listId, accountIds }) {
|
||||||
if (!state.allListsObject[id]) {
|
if (!state.allListsObject[listId]) {
|
||||||
state.allListsObject[id] = {}
|
state.allListsObject[listId] = { accountIds: [] }
|
||||||
}
|
}
|
||||||
state.allListsObject[id].accountIds = accountIds
|
state.allListsObject[listId].accountIds = accountIds
|
||||||
},
|
},
|
||||||
deleteList (state, { id }) {
|
addListAccount (state, { listId, accountId }) {
|
||||||
delete state.allListsObject[id]
|
if (!state.allListsObject[listId]) {
|
||||||
remove(state.allLists, list => list.id === id)
|
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 }) {
|
createList ({ rootState, commit }, { title }) {
|
||||||
return rootState.api.backendInteractor.createList({ title })
|
return rootState.api.backendInteractor.createList({ title })
|
||||||
.then((list) => {
|
.then((list) => {
|
||||||
commit('setList', { id: list.id, title })
|
commit('setList', { listId: list.id, title })
|
||||||
return list
|
return list
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fetchList ({ rootState, commit }, { id }) {
|
fetchList ({ rootState, commit }, { listId }) {
|
||||||
return rootState.api.backendInteractor.getList({ id })
|
return rootState.api.backendInteractor.getList({ listId })
|
||||||
.then((list) => commit('setList', { id: list.id, title: list.title }))
|
.then((list) => commit('setList', { listId: list.id, title: list.title }))
|
||||||
},
|
},
|
||||||
fetchListAccounts ({ rootState, commit }, { id }) {
|
fetchListAccounts ({ rootState, commit }, { listId }) {
|
||||||
return rootState.api.backendInteractor.getListAccounts({ id })
|
return rootState.api.backendInteractor.getListAccounts({ listId })
|
||||||
.then((accountIds) => commit('setListAccounts', { id, accountIds }))
|
.then((accountIds) => commit('setListAccounts', { listId, accountIds }))
|
||||||
},
|
},
|
||||||
setList ({ rootState, commit }, { id, title }) {
|
setList ({ rootState, commit }, { listId, title }) {
|
||||||
rootState.api.backendInteractor.updateList({ id, title })
|
rootState.api.backendInteractor.updateList({ listId, title })
|
||||||
commit('setList', { id, title })
|
commit('setList', { listId, title })
|
||||||
},
|
},
|
||||||
setListAccounts ({ rootState, commit }, { id, accountIds }) {
|
setListAccounts ({ rootState, commit }, { listId, accountIds }) {
|
||||||
const saved = rootState.lists.allListsObject[id].accountIds || []
|
const saved = rootState.lists.allListsObject[listId].accountIds || []
|
||||||
const added = accountIds.filter(id => !saved.includes(id))
|
const added = accountIds.filter(id => !saved.includes(id))
|
||||||
const removed = saved.filter(id => !accountIds.includes(id))
|
const removed = saved.filter(id => !accountIds.includes(id))
|
||||||
commit('setListAccounts', { id, accountIds })
|
commit('setListAccounts', { listId, accountIds })
|
||||||
if (added.length > 0) {
|
if (added.length > 0) {
|
||||||
rootState.api.backendInteractor.addAccountsToList({ id, accountIds: added })
|
rootState.api.backendInteractor.addAccountsToList({ listId, accountIds: added })
|
||||||
}
|
}
|
||||||
if (removed.length > 0) {
|
if (removed.length > 0) {
|
||||||
rootState.api.backendInteractor.removeAccountsFromList({ id, accountIds: removed })
|
rootState.api.backendInteractor.removeAccountsFromList({ listId, accountIds: removed })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteList ({ rootState, commit }, { id }) {
|
addListAccount ({ rootState, commit }, { listId, accountId }) {
|
||||||
rootState.api.backendInteractor.deleteList({ id })
|
return rootState
|
||||||
commit('deleteList', { id })
|
.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 { 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'
|
import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js'
|
||||||
|
|
||||||
export const VERSION = 1
|
export const VERSION = 1
|
||||||
|
@ -14,14 +14,21 @@ export const defaultState = {
|
||||||
// storage of flags - stuff that can only be set and incremented
|
// storage of flags - stuff that can only be set and incremented
|
||||||
flagStorage: {
|
flagStorage: {
|
||||||
updateCounter: 0, // Counter for most recent update notification seen
|
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
|
reset: 0 // special flag that can be used to force-reset all flags, debug purposes only
|
||||||
// special reset codes:
|
// special reset codes:
|
||||||
// 1000: trim keys to those known by currently running FE
|
// 1000: trim keys to those known by currently running FE
|
||||||
// 1001: same as above + reset everything to 0
|
// 1001: same as above + reset everything to 0
|
||||||
},
|
},
|
||||||
|
prefsStorage: {
|
||||||
|
_journal: [],
|
||||||
|
simple: {
|
||||||
|
dontShowUpdateNotifs: false,
|
||||||
|
collapseNav: false
|
||||||
|
},
|
||||||
|
collections: {
|
||||||
|
pinnedNavItems: ['home', 'dms', 'chats']
|
||||||
|
}
|
||||||
|
},
|
||||||
// raw data
|
// raw data
|
||||||
raw: null,
|
raw: null,
|
||||||
// local cache
|
// local cache
|
||||||
|
@ -33,14 +40,43 @@ export const newUserFlags = {
|
||||||
updateCounter: CURRENT_UPDATE_COUNTER // new users don't need to see update notification
|
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,
|
...data,
|
||||||
|
_user: userName,
|
||||||
_timestamp: Date.now(),
|
_timestamp: Date.now(),
|
||||||
_version: VERSION
|
_version: VERSION
|
||||||
})
|
})
|
||||||
|
|
||||||
const _checkValidity = (data) => data._timestamp > 0 && data._version > 0
|
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) => {
|
export const _getRecentData = (cache, live) => {
|
||||||
const result = { recent: null, stale: null, needUpload: false }
|
const result = { recent: null, stale: null, needUpload: false }
|
||||||
const cacheValid = _checkValidity(cache || {})
|
const cacheValid = _checkValidity(cache || {})
|
||||||
|
@ -85,6 +121,8 @@ export const _getAllFlags = (recent, stale) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const _mergeFlags = (recent, stale, allFlagKeys) => {
|
export const _mergeFlags = (recent, stale, allFlagKeys) => {
|
||||||
|
if (!stale.flagStorage) return recent.flagStorage
|
||||||
|
if (!recent.flagStorage) return stale.flagStorage
|
||||||
return Object.fromEntries(allFlagKeys.map(flag => {
|
return Object.fromEntries(allFlagKeys.map(flag => {
|
||||||
const recentFlag = recent.flagStorage[flag]
|
const recentFlag = recent.flagStorage[flag]
|
||||||
const staleFlag = stale.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) => {
|
export const _resetFlags = (totalFlags, knownKeys = defaultState.flagStorage) => {
|
||||||
let result = { ...totalFlags }
|
let result = { ...totalFlags }
|
||||||
const allFlagKeys = Object.keys(totalFlags)
|
const allFlagKeys = Object.keys(totalFlags)
|
||||||
|
@ -149,10 +269,17 @@ export const _doMigrations = (cache) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
clearServerSideStorage (state, userData) {
|
||||||
|
state = { ...cloneDeep(defaultState) }
|
||||||
|
},
|
||||||
setServerSideStorage (state, userData) {
|
setServerSideStorage (state, userData) {
|
||||||
const live = userData.storage
|
const live = userData.storage
|
||||||
state.raw = live
|
state.raw = live
|
||||||
let cache = state.cache
|
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)
|
cache = _doMigrations(cache)
|
||||||
|
|
||||||
|
@ -165,7 +292,8 @@ export const mutations = {
|
||||||
if (recent === null) {
|
if (recent === null) {
|
||||||
console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`)
|
console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`)
|
||||||
recent = _wrapData({
|
recent = _wrapData({
|
||||||
flagStorage: { ...flagsTemplate }
|
flagStorage: { ...flagsTemplate },
|
||||||
|
prefsStorage: { ...defaultState.prefsStorage }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,17 +308,23 @@ export const mutations = {
|
||||||
|
|
||||||
const allFlagKeys = _getAllFlags(recent, stale)
|
const allFlagKeys = _getAllFlags(recent, stale)
|
||||||
let totalFlags
|
let totalFlags
|
||||||
|
let totalPrefs
|
||||||
if (dirty) {
|
if (dirty) {
|
||||||
// Merge the flags
|
// Merge the flags
|
||||||
console.debug('Merging the flags...')
|
console.debug('Merging the data...')
|
||||||
totalFlags = _mergeFlags(recent, stale, allFlagKeys)
|
totalFlags = _mergeFlags(recent, stale, allFlagKeys)
|
||||||
|
_verifyPrefs(recent)
|
||||||
|
_verifyPrefs(stale)
|
||||||
|
totalPrefs = _mergePrefs(recent.prefsStorage, stale.prefsStorage)
|
||||||
} else {
|
} else {
|
||||||
totalFlags = recent.flagStorage
|
totalFlags = recent.flagStorage
|
||||||
|
totalPrefs = recent.prefsStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
totalFlags = _resetFlags(totalFlags)
|
totalFlags = _resetFlags(totalFlags)
|
||||||
|
|
||||||
recent.flagStorage = totalFlags
|
recent.flagStorage = { ...flagsTemplate, ...totalFlags }
|
||||||
|
recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs }
|
||||||
|
|
||||||
state.dirty = dirty || needsUpload
|
state.dirty = dirty || needsUpload
|
||||||
state.cache = recent
|
state.cache = recent
|
||||||
|
@ -199,10 +333,72 @@ export const mutations = {
|
||||||
state.cache._timestamp = Math.min(stale._timestamp, recent._timestamp)
|
state.cache._timestamp = Math.min(stale._timestamp, recent._timestamp)
|
||||||
}
|
}
|
||||||
state.flagStorage = state.cache.flagStorage
|
state.flagStorage = state.cache.flagStorage
|
||||||
|
state.prefsStorage = state.cache.prefsStorage
|
||||||
},
|
},
|
||||||
setFlag (state, { flag, value }) {
|
setFlag (state, { flag, value }) {
|
||||||
state.flagStorage[flag] = value
|
state.flagStorage[flag] = value
|
||||||
state.dirty = true
|
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: {
|
actions: {
|
||||||
pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) {
|
pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) {
|
||||||
const needPush = state.dirty || force
|
const needPush = state.dirty || force
|
||||||
|
console.log(needPush)
|
||||||
if (!needPush) return
|
if (!needPush) return
|
||||||
state.cache = _wrapData({
|
commit('updateCache', { username: rootState.users.currentUser.fqn })
|
||||||
flagStorage: toRaw(state.flagStorage)
|
|
||||||
})
|
|
||||||
const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } }
|
const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } }
|
||||||
rootState.api.backendInteractor
|
rootState.api.backendInteractor
|
||||||
.updateProfile({ params })
|
.updateProfile({ params })
|
||||||
.then((user) => commit('setServerSideStorage', user))
|
.then((user) => {
|
||||||
state.dirty = false
|
commit('setServerSideStorage', user)
|
||||||
|
state.dirty = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,6 +171,9 @@ export const mutations = {
|
||||||
state.relationships[relationship.id] = relationship
|
state.relationships[relationship.id] = relationship
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
updateUserInLists (state, { id, inLists }) {
|
||||||
|
state.usersObject[id].inLists = inLists
|
||||||
|
},
|
||||||
saveBlockIds (state, blockIds) {
|
saveBlockIds (state, blockIds) {
|
||||||
state.currentUser.blockIds = blockIds
|
state.currentUser.blockIds = blockIds
|
||||||
},
|
},
|
||||||
|
@ -298,6 +301,12 @@ const users = {
|
||||||
.then((relationships) => store.commit('updateUserRelationship', relationships))
|
.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) {
|
fetchBlocks (store) {
|
||||||
return store.rootState.api.backendInteractor.fetchBlocks()
|
return store.rootState.api.backendInteractor.fetchBlocks()
|
||||||
.then((blocks) => {
|
.then((blocks) => {
|
||||||
|
@ -509,6 +518,7 @@ const users = {
|
||||||
store.dispatch('stopFetchingTimeline', 'friends')
|
store.dispatch('stopFetchingTimeline', 'friends')
|
||||||
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
||||||
store.dispatch('stopFetchingNotifications')
|
store.dispatch('stopFetchingNotifications')
|
||||||
|
store.dispatch('stopFetchingLists')
|
||||||
store.dispatch('stopFetchingFollowRequests')
|
store.dispatch('stopFetchingFollowRequests')
|
||||||
store.commit('clearNotifications')
|
store.commit('clearNotifications')
|
||||||
store.commit('resetStatuses')
|
store.commit('resetStatuses')
|
||||||
|
@ -516,6 +526,7 @@ const users = {
|
||||||
store.dispatch('setLastTimeline', 'public-timeline')
|
store.dispatch('setLastTimeline', 'public-timeline')
|
||||||
store.dispatch('setLayoutWidth', windowWidth())
|
store.dispatch('setLayoutWidth', windowWidth())
|
||||||
store.dispatch('setLayoutHeight', windowHeight())
|
store.dispatch('setLayoutHeight', windowHeight())
|
||||||
|
store.commit('clearServerSideStorage')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
loginUser (store, accessToken) {
|
loginUser (store, accessToken) {
|
||||||
|
@ -562,6 +573,12 @@ const users = {
|
||||||
store.dispatch('startFetchingChats')
|
store.dispatch('startFetchingChats')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
store.dispatch('startFetchingLists')
|
||||||
|
|
||||||
|
if (user.locked) {
|
||||||
|
store.dispatch('startFetchingFollowRequests')
|
||||||
|
}
|
||||||
|
|
||||||
if (store.getters.mergedConfig.useStreamingApi) {
|
if (store.getters.mergedConfig.useStreamingApi) {
|
||||||
store.dispatch('fetchTimeline', 'friends', { since: null })
|
store.dispatch('fetchTimeline', 'friends', { since: null })
|
||||||
store.dispatch('fetchNotifications', { since: null })
|
store.dispatch('fetchNotifications', { since: null })
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
.panel-footer {
|
.panel-footer {
|
||||||
--panel-heading-height-padding: 0.6em;
|
--panel-heading-height-padding: 0.6em;
|
||||||
--__panel-heading-height: 3.2em;
|
--__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;
|
position: relative;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
grid-column-gap: 0.5em;
|
grid-column-gap: 0.5em;
|
||||||
flex: none;
|
flex: none;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
padding: 0.6em;
|
padding: var(--panel-heading-height-padding);
|
||||||
height: var(--__panel-heading-height);
|
height: var(--__panel-heading-height);
|
||||||
line-height: var(--__panel-heading-height-inner);
|
line-height: var(--__panel-heading-height-inner);
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
|
@ -147,6 +147,15 @@
|
||||||
color: var(--panelLink, $fallback--link);
|
color: var(--panelLink, $fallback--link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-unstyled:hover,
|
||||||
|
a:hover {
|
||||||
|
i[class*=icon-],
|
||||||
|
.svg-inline--fa,
|
||||||
|
.iconLetter {
|
||||||
|
color: var(--panelText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.faint {
|
.faint {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: $fallback--faint;
|
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_LOOKUP_URL = '/api/v1/accounts/lookup'
|
||||||
const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
|
const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
|
||||||
const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
|
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_URL = id => `/api/v1/lists/${id}`
|
||||||
const MASTODON_LIST_TIMELINE_URL = id => `/api/v1/timelines/list/${id}`
|
const MASTODON_LIST_TIMELINE_URL = id => `/api/v1/timelines/list/${id}`
|
||||||
const MASTODON_LIST_ACCOUNTS_URL = id => `/api/v1/lists/${id}/accounts`
|
const MASTODON_LIST_ACCOUNTS_URL = id => `/api/v1/lists/${id}/accounts`
|
||||||
|
@ -263,6 +264,13 @@ const unfollowUser = ({ id, credentials }) => {
|
||||||
}).then((data) => data.json())
|
}).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 }) => {
|
const pinOwnStatus = ({ id, credentials }) => {
|
||||||
return promisedRequest({ url: MASTODON_PIN_OWN_STATUS(id), credentials, method: 'POST' })
|
return promisedRequest({ url: MASTODON_PIN_OWN_STATUS(id), credentials, method: 'POST' })
|
||||||
.then((data) => parseStatus(data))
|
.then((data) => parseStatus(data))
|
||||||
|
@ -428,14 +436,14 @@ const createList = ({ title, credentials }) => {
|
||||||
}).then((data) => data.json())
|
}).then((data) => data.json())
|
||||||
}
|
}
|
||||||
|
|
||||||
const getList = ({ id, credentials }) => {
|
const getList = ({ listId, credentials }) => {
|
||||||
const url = MASTODON_LIST_URL(id)
|
const url = MASTODON_LIST_URL(listId)
|
||||||
return fetch(url, { headers: authHeaders(credentials) })
|
return fetch(url, { headers: authHeaders(credentials) })
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateList = ({ id, title, credentials }) => {
|
const updateList = ({ listId, title, credentials }) => {
|
||||||
const url = MASTODON_LIST_URL(id)
|
const url = MASTODON_LIST_URL(listId)
|
||||||
const headers = authHeaders(credentials)
|
const headers = authHeaders(credentials)
|
||||||
headers['Content-Type'] = 'application/json'
|
headers['Content-Type'] = 'application/json'
|
||||||
|
|
||||||
|
@ -446,15 +454,15 @@ const updateList = ({ id, title, credentials }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getListAccounts = ({ id, credentials }) => {
|
const getListAccounts = ({ listId, credentials }) => {
|
||||||
const url = MASTODON_LIST_ACCOUNTS_URL(id)
|
const url = MASTODON_LIST_ACCOUNTS_URL(listId)
|
||||||
return fetch(url, { headers: authHeaders(credentials) })
|
return fetch(url, { headers: authHeaders(credentials) })
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((data) => data.map(({ id }) => id))
|
.then((data) => data.map(({ id }) => id))
|
||||||
}
|
}
|
||||||
|
|
||||||
const addAccountsToList = ({ id, accountIds, credentials }) => {
|
const addAccountsToList = ({ listId, accountIds, credentials }) => {
|
||||||
const url = MASTODON_LIST_ACCOUNTS_URL(id)
|
const url = MASTODON_LIST_ACCOUNTS_URL(listId)
|
||||||
const headers = authHeaders(credentials)
|
const headers = authHeaders(credentials)
|
||||||
headers['Content-Type'] = 'application/json'
|
headers['Content-Type'] = 'application/json'
|
||||||
|
|
||||||
|
@ -465,8 +473,8 @@ const addAccountsToList = ({ id, accountIds, credentials }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeAccountsFromList = ({ id, accountIds, credentials }) => {
|
const removeAccountsFromList = ({ listId, accountIds, credentials }) => {
|
||||||
const url = MASTODON_LIST_ACCOUNTS_URL(id)
|
const url = MASTODON_LIST_ACCOUNTS_URL(listId)
|
||||||
const headers = authHeaders(credentials)
|
const headers = authHeaders(credentials)
|
||||||
headers['Content-Type'] = 'application/json'
|
headers['Content-Type'] = 'application/json'
|
||||||
|
|
||||||
|
@ -477,8 +485,8 @@ const removeAccountsFromList = ({ id, accountIds, credentials }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteList = ({ id, credentials }) => {
|
const deleteList = ({ listId, credentials }) => {
|
||||||
const url = MASTODON_LIST_URL(id)
|
const url = MASTODON_LIST_URL(listId)
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: authHeaders(credentials)
|
headers: authHeaders(credentials)
|
||||||
|
@ -1584,7 +1592,8 @@ const apiService = {
|
||||||
sendChatMessage,
|
sendChatMessage,
|
||||||
readChat,
|
readChat,
|
||||||
deleteChatMessage,
|
deleteChatMessage,
|
||||||
setReportState
|
setReportState,
|
||||||
|
fetchUserInLists
|
||||||
}
|
}
|
||||||
|
|
||||||
export default apiService
|
export default apiService
|
||||||
|
|
|
@ -43,11 +43,13 @@ export const parseUser = (data) => {
|
||||||
// case for users in "mentions" property for statuses in MastoAPI
|
// case for users in "mentions" property for statuses in MastoAPI
|
||||||
const mastoShort = masto && !Object.prototype.hasOwnProperty.call(data, 'avatar')
|
const mastoShort = masto && !Object.prototype.hasOwnProperty.call(data, 'avatar')
|
||||||
|
|
||||||
|
output.inLists = null
|
||||||
output.id = String(data.id)
|
output.id = String(data.id)
|
||||||
output._original = data // used for server-side settings
|
output._original = data // used for server-side settings
|
||||||
|
|
||||||
if (masto) {
|
if (masto) {
|
||||||
output.screen_name = data.acct
|
output.screen_name = data.acct
|
||||||
|
output.fqn = data.fqn
|
||||||
output.statusnet_profile_url = data.url
|
output.statusnet_profile_url = data.url
|
||||||
|
|
||||||
// There's nothing else to get
|
// There's nothing else to get
|
||||||
|
@ -214,12 +216,14 @@ export const parseUser = (data) => {
|
||||||
output.screen_name_ui = output.screen_name
|
output.screen_name_ui = output.screen_name
|
||||||
if (output.screen_name && output.screen_name.includes('@')) {
|
if (output.screen_name && output.screen_name.includes('@')) {
|
||||||
const parts = output.screen_name.split('@')
|
const parts = output.screen_name.split('@')
|
||||||
let unicodeDomain = punycode.toUnicode(parts[1])
|
const unicodeDomain = punycode.toUnicode(parts[1])
|
||||||
if (unicodeDomain !== parts[1]) {
|
if (unicodeDomain !== parts[1]) {
|
||||||
// Add some identifier so users can potentially spot spoofing attempts:
|
// Add some identifier so users can potentially spot spoofing attempts:
|
||||||
// lain.com and xn--lin-6cd.com would appear identical otherwise.
|
// 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('@')
|
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()
|
rules: utils.styleLoaders()
|
||||||
},
|
},
|
||||||
devtool: 'inline-source-map',
|
devtool: 'inline-source-map',
|
||||||
// vue: {
|
|
||||||
// loaders: {
|
|
||||||
// js: 'isparta'
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': require('../../config/test.env')
|
'process.env': require('../../config/test.env')
|
||||||
|
@ -37,22 +32,6 @@ const webpackConfig = merge(baseConfig, {
|
||||||
// no need for app entry during tests
|
// no need for app entry during tests
|
||||||
delete webpackConfig.entry
|
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) {
|
module.exports = function (config) {
|
||||||
config.set({
|
config.set({
|
||||||
// to run in additional browsers:
|
// to run in additional browsers:
|
||||||
|
|
|
@ -17,13 +17,13 @@ describe('The lists module', () => {
|
||||||
const list = { id: '1', title: 'testList' }
|
const list = { id: '1', title: 'testList' }
|
||||||
const modList = { id: '1', title: 'anotherTestTitle' }
|
const modList = { id: '1', title: 'anotherTestTitle' }
|
||||||
|
|
||||||
mutations.setList(state, list)
|
mutations.setList(state, { listId: list.id, title: list.title })
|
||||||
expect(state.allListsObject[list.id]).to.eql({ title: list.title })
|
expect(state.allListsObject[list.id]).to.eql({ title: list.title, accountIds: [] })
|
||||||
expect(state.allLists).to.have.length(1)
|
expect(state.allLists).to.have.length(1)
|
||||||
expect(state.allLists[0]).to.eql(list)
|
expect(state.allLists[0]).to.eql(list)
|
||||||
|
|
||||||
mutations.setList(state, modList)
|
mutations.setList(state, { listId: modList.id, title: modList.title })
|
||||||
expect(state.allListsObject[modList.id]).to.eql({ title: modList.title })
|
expect(state.allListsObject[modList.id]).to.eql({ title: modList.title, accountIds: [] })
|
||||||
expect(state.allLists).to.have.length(1)
|
expect(state.allLists).to.have.length(1)
|
||||||
expect(state.allLists[0]).to.eql(modList)
|
expect(state.allLists[0]).to.eql(modList)
|
||||||
})
|
})
|
||||||
|
@ -33,10 +33,10 @@ describe('The lists module', () => {
|
||||||
const list = { id: '1', accountIds: ['1', '2', '3'] }
|
const list = { id: '1', accountIds: ['1', '2', '3'] }
|
||||||
const modList = { id: '1', accountIds: ['3', '4', '5'] }
|
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 })
|
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 })
|
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'] }
|
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.allLists).to.have.length(0)
|
||||||
expect(state.allListsObject).to.eql({})
|
expect(state.allListsObject).to.eql({})
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,9 +4,11 @@ import {
|
||||||
VERSION,
|
VERSION,
|
||||||
COMMAND_TRIM_FLAGS,
|
COMMAND_TRIM_FLAGS,
|
||||||
COMMAND_TRIM_FLAGS_AND_RESET,
|
COMMAND_TRIM_FLAGS_AND_RESET,
|
||||||
|
_moveItemInArray,
|
||||||
_getRecentData,
|
_getRecentData,
|
||||||
_getAllFlags,
|
_getAllFlags,
|
||||||
_mergeFlags,
|
_mergeFlags,
|
||||||
|
_mergePrefs,
|
||||||
_resetFlags,
|
_resetFlags,
|
||||||
mutations,
|
mutations,
|
||||||
defaultState,
|
defaultState,
|
||||||
|
@ -28,6 +30,7 @@ describe('The serverSideStorage module', () => {
|
||||||
expect(state.cache._version).to.eql(VERSION)
|
expect(state.cache._version).to.eql(VERSION)
|
||||||
expect(state.cache._timestamp).to.be.a('number')
|
expect(state.cache._timestamp).to.be.a('number')
|
||||||
expect(state.cache.flagStorage).to.eql(defaultState.flagStorage)
|
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', () => {
|
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._version).to.eql(VERSION)
|
||||||
expect(state.cache._timestamp).to.be.a('number')
|
expect(state.cache._timestamp).to.be.a('number')
|
||||||
expect(state.cache.flagStorage).to.eql(newUserFlags)
|
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', () => {
|
it('should merge flags even if remote timestamp is older', () => {
|
||||||
|
@ -57,6 +61,9 @@ describe('The serverSideStorage module', () => {
|
||||||
flagStorage: {
|
flagStorage: {
|
||||||
...defaultState.flagStorage,
|
...defaultState.flagStorage,
|
||||||
updateCounter: 1
|
updateCounter: 1
|
||||||
|
},
|
||||||
|
prefsStorage: {
|
||||||
|
...defaultState.prefsStorage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,9 +106,62 @@ describe('The serverSideStorage module', () => {
|
||||||
expect(state.cache.flagStorage).to.eql(defaultState.flagStorage)
|
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('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', () => {
|
describe('_getRecentData', () => {
|
||||||
it('should handle nulls correctly', () => {
|
it('should handle nulls correctly', () => {
|
||||||
expect(_getRecentData(null, null)).to.eql({ recent: null, stale: null, needUpload: true })
|
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', () => {
|
describe('_resetFlags', () => {
|
||||||
it('should reset all known flags to 0 when reset flag is set to > 0 and < 9000', () => {
|
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 }
|
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', () => {
|
it('converts IDN to unicode and marks it as internatonal', () => {
|
||||||
const user = makeMockUserMasto({ acct: 'lain@xn--lin-6cd.com' })
|
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 New Issue