aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHenry Jameson <me@hjkos.com>2022-08-30 23:54:16 +0300
committerHenry Jameson <me@hjkos.com>2022-08-30 23:54:16 +0300
commit887fac5addc2416504593cb62a52a3b08f3638e2 (patch)
tree74711c1bc7ec9fe991ad15d51b3ecfab7e5e0749 /src
parentaf734afe3694ff3c77d4457b93b02309c4f55d6d (diff)
parent8b25febe36a97d113c846928dab22ab36158ee07 (diff)
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 ...
Diffstat (limited to 'src')
-rw-r--r--src/App.js4
-rw-r--r--src/App.scss26
-rw-r--r--src/App.vue2
-rw-r--r--src/boot/routes.js5
-rw-r--r--src/components/account_actions/account_actions.js4
-rw-r--r--src/components/account_actions/account_actions.vue1
-rw-r--r--src/components/basic_user_card/basic_user_card.js4
-rw-r--r--src/components/basic_user_card/basic_user_card.vue8
-rw-r--r--src/components/desktop_nav/desktop_nav.scss4
-rw-r--r--src/components/desktop_nav/desktop_nav.vue1
-rw-r--r--src/components/emoji_input/emoji_input.js4
-rw-r--r--src/components/emoji_input/emoji_input.vue16
-rw-r--r--src/components/emoji_input/suggestor.js11
-rw-r--r--src/components/lists/lists.js7
-rw-r--r--src/components/lists/lists.vue24
-rw-r--r--src/components/lists_edit/lists_edit.js110
-rw-r--r--src/components/lists_edit/lists_edit.vue232
-rw-r--r--src/components/lists_menu/lists_menu_content.js29
-rw-r--r--src/components/lists_menu/lists_menu_content.vue17
-rw-r--r--src/components/lists_new/lists_new.js79
-rw-r--r--src/components/lists_new/lists_new.vue95
-rw-r--r--src/components/lists_timeline/lists_timeline.js4
-rw-r--r--src/components/lists_user_search/lists_user_search.js7
-rw-r--r--src/components/lists_user_search/lists_user_search.vue20
-rw-r--r--src/components/mention_link/mention_link.js2
-rw-r--r--src/components/mention_link/mention_link.vue3
-rw-r--r--src/components/mobile_nav/mobile_nav.js9
-rw-r--r--src/components/mobile_nav/mobile_nav.vue29
-rw-r--r--src/components/mobile_post_status_button/mobile_post_status_button.js3
-rw-r--r--src/components/nav_panel/nav_panel.js75
-rw-r--r--src/components/nav_panel/nav_panel.vue262
-rw-r--r--src/components/navigation/filter.js18
-rw-r--r--src/components/navigation/navigation.js75
-rw-r--r--src/components/navigation/navigation_entry.js47
-rw-r--r--src/components/navigation/navigation_entry.vue120
-rw-r--r--src/components/navigation/navigation_pins.js88
-rw-r--r--src/components/navigation/navigation_pins.vue76
-rw-r--r--src/components/notification/notification.js4
-rw-r--r--src/components/notification/notification.vue21
-rw-r--r--src/components/popover/popover.js25
-rw-r--r--src/components/popover/popover.vue7
-rw-r--r--src/components/search_bar/search_bar.vue2
-rw-r--r--src/components/settings_modal/tabs/general_tab.vue5
-rw-r--r--src/components/side_drawer/side_drawer.js13
-rw-r--r--src/components/side_drawer/side_drawer.vue14
-rw-r--r--src/components/status/status.js4
-rw-r--r--src/components/status/status.vue16
-rw-r--r--src/components/tab_switcher/tab_switcher.scss1
-rw-r--r--src/components/timeline/timeline.vue5
-rw-r--r--src/components/timeline_menu/timeline_menu.js16
-rw-r--r--src/components/timeline_menu/timeline_menu.vue17
-rw-r--r--src/components/timeline_menu/timeline_menu_content.js29
-rw-r--r--src/components/timeline_menu/timeline_menu_content.vue66
-rw-r--r--src/components/unicode_domain_indicator/unicode_domain_indicator.vue26
-rw-r--r--src/components/update_notification/update_notification.js4
-rw-r--r--src/components/update_notification/update_notification.vue2
-rw-r--r--src/components/user_card/user_card.js4
-rw-r--r--src/components/user_card/user_card.vue9
-rw-r--r--src/components/user_link/user_link.vue38
-rw-r--r--src/components/user_list_menu/user_list_menu.js93
-rw-r--r--src/components/user_list_menu/user_list_menu.vue38
-rw-r--r--src/components/user_list_popover/user_list_popover.js2
-rw-r--r--src/components/user_list_popover/user_list_popover.vue2
-rw-r--r--src/components/user_reporting_modal/user_reporting_modal.js4
-rw-r--r--src/components/user_reporting_modal/user_reporting_modal.vue10
-rw-r--r--src/i18n/en.json26
-rw-r--r--src/modules/api.js3
-rw-r--r--src/modules/config.js1
-rw-r--r--src/modules/lists.js100
-rw-r--r--src/modules/serverSideStorage.js223
-rw-r--r--src/modules/users.js17
-rw-r--r--src/panel.scss13
-rw-r--r--src/services/api/api.service.js35
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js8
74 files changed, 1679 insertions, 745 deletions
diff --git a/src/App.js b/src/App.js
index d1ad16d5..18dd61f2 100644
--- a/src/App.js
+++ b/src/App.js
@@ -92,8 +92,12 @@ export default {
isChats () {
return this.$route.name === 'chat' || this.$route.name === 'chats'
},
+ isListEdit () {
+ return this.$route.name === 'lists-edit'
+ },
newPostButtonShown () {
if (this.isChats) return false
+ if (this.isListEdit) return false
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
},
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
diff --git a/src/App.scss b/src/App.scss
index dec187af..75b2667c 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -117,12 +117,28 @@ h4 {
margin: 0;
}
+.iconLetter {
+ display: inline-block;
+ text-align: center;
+ font-weight: 1000;
+}
+
i[class*=icon-],
-.svg-inline--fa {
+.svg-inline--fa,
+.iconLetter {
color: $fallback--icon;
color: var(--icon, $fallback--icon);
}
+.button-unstyled:hover,
+a:hover {
+ > i[class*=icon-],
+ > .svg-inline--fa,
+ > .iconLetter {
+ color: var(--text);
+ }
+}
+
nav {
z-index: var(--ZI_navbar);
color: var(--topBarText);
@@ -765,17 +781,23 @@ option {
}
.fa-scale-110 {
- &.svg-inline--fa {
+ &.svg-inline--fa,
+ &.iconLetter {
font-size: 1.1em;
}
}
.fa-old-padding {
+ &.iconLetter,
&.svg-inline--fa, &-layer {
padding: 0 0.3em;
}
}
+.veryfaint {
+ opacity: 0.25;
+}
+
.login-hint {
text-align: center;
diff --git a/src/App.vue b/src/App.vue
index 1f96efe8..a993c238 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -36,7 +36,7 @@
<div
id="main-scroller"
class="column main"
- :class="{ '-full-height': isChats }"
+ :class="{ '-full-height': isChats || isListEdit }"
>
<div
v-if="!currentUser"
diff --git a/src/boot/routes.js b/src/boot/routes.js
index 91588af2..63dd1297 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -23,6 +23,7 @@ import RemoteUserResolver from 'components/remote_user_resolver/remote_user_reso
import Lists from 'components/lists/lists.vue'
import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
import ListsEdit from 'components/lists_edit/lists_edit.vue'
+import NavPanel from 'src/components/nav_panel/nav_panel.vue'
export default (store) => {
const validateAuthenticatedRoute = (to, from, next) => {
@@ -79,7 +80,9 @@ export default (store) => {
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile },
{ name: 'lists', path: '/lists', component: Lists },
{ name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline },
- { name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit }
+ { name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit },
+ { name: 'lists-new', path: '/lists/new', component: ListsEdit },
+ { name: 'edit-navigation', path: '/nav-edit', component: NavPanel, props: () => ({ forceExpand: true, forceEditMode: true }), beforeEnter: validateAuthenticatedRoute }
]
if (store.state.instance.pleromaChatMessagesAvailable) {
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js
index 99762562..735dd81c 100644
--- a/src/components/account_actions/account_actions.js
+++ b/src/components/account_actions/account_actions.js
@@ -1,6 +1,7 @@
import { mapState } from 'vuex'
import ProgressButton from '../progress_button/progress_button.vue'
import Popover from '../popover/popover.vue'
+import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEllipsisV
@@ -19,7 +20,8 @@ const AccountActions = {
},
components: {
ProgressButton,
- Popover
+ Popover,
+ UserListMenu
},
methods: {
showRepeats () {
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index 23547f2c..770740e0 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -28,6 +28,7 @@
class="dropdown-divider"
/>
</template>
+ <UserListMenu :user="user" />
<button
v-if="relationship.blocking"
class="btn button-default btn-block dropdown-item"
diff --git a/src/components/basic_user_card/basic_user_card.js b/src/components/basic_user_card/basic_user_card.js
index 8b1a2c38..31de2d75 100644
--- a/src/components/basic_user_card/basic_user_card.js
+++ b/src/components/basic_user_card/basic_user_card.js
@@ -1,5 +1,6 @@
import UserPopover from '../user_popover/user_popover.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
+import UserLink from '../user_link/user_link.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@@ -10,7 +11,8 @@ const BasicUserCard = {
components: {
UserPopover,
UserAvatar,
- RichContent
+ RichContent,
+ UserLink
},
methods: {
userProfileLink (user) {
diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index 9cca7840..418de926 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -30,12 +30,10 @@
/>
</div>
<div>
- <router-link
+ <user-link
class="basic-user-card-screen-name"
- :to="userProfileLink(user)"
- >
- @{{ user.screen_name_ui }}
- </router-link>
+ :user="user"
+ />
</div>
<slot />
</div>
diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss
index f6c7c43d..1ec25385 100644
--- a/src/components/desktop_nav/desktop_nav.scss
+++ b/src/components/desktop_nav/desktop_nav.scss
@@ -137,4 +137,8 @@
text-align: right;
}
}
+
+ .spacer {
+ width: 1em;
+ }
}
diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue
index f352c78c..5db7fc79 100644
--- a/src/components/desktop_nav/desktop_nav.vue
+++ b/src/components/desktop_nav/desktop_nav.vue
@@ -61,6 +61,7 @@
:title="$t('nav.administration')"
/>
</a>
+ <span class="spacer" />
<button
v-if="currentUser"
class="button-unstyled nav-icon"
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 5ba3907f..b664d6b3 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -1,5 +1,6 @@
import Completion from '../../services/completion/completion.js'
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
+import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import { take } from 'lodash'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
@@ -120,7 +121,8 @@ const EmojiInput = {
}
},
components: {
- EmojiPicker
+ EmojiPicker,
+ UnicodeDomainIndicator
},
computed: {
padEmoji () {
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index 7d95ab7e..81b81913 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -50,7 +50,21 @@
<span v-else>{{ suggestion.replacement }}</span>
</span>
<div class="label">
- <span class="displayText">{{ suggestion.displayText }}</span>
+ <span
+ v-if="suggestion.user"
+ class="displayText"
+ >
+ {{ suggestion.displayText }}<UnicodeDomainIndicator
+ :user="suggestion.user"
+ :at="false"
+ />
+ </span>
+ <span
+ v-if="!suggestion.user"
+ class="displayText"
+ >
+ {{ suggestion.displayText }}
+ </span>
<span class="detailText">{{ suggestion.detailText }}</span>
</div>
</div>
diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js
index e8efbd1e..0ddb4d68 100644
--- a/src/components/emoji_input/suggestor.js
+++ b/src/components/emoji_input/suggestor.js
@@ -116,11 +116,12 @@ export const suggestUsers = ({ dispatch, state }) => {
return diff + nameAlphabetically + screenNameAlphabetically
/* eslint-disable camelcase */
- }).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
- displayText: screen_name_ui,
- detailText: name,
- imageUrl: profile_image_url_original,
- replacement: '@' + screen_name + ' '
+ }).map((user) => ({
+ user,
+ displayText: user.screen_name_ui,
+ detailText: user.name,
+ imageUrl: user.profile_image_url_original,
+ replacement: '@' + user.screen_name + ' '
}))
/* eslint-enable camelcase */
diff --git a/src/components/lists/lists.js b/src/components/lists/lists.js
index 791b99b2..56d68430 100644
--- a/src/components/lists/lists.js
+++ b/src/components/lists/lists.js
@@ -1,5 +1,4 @@
import ListsCard from '../lists_card/lists_card.vue'
-import ListsNew from '../lists_new/lists_new.vue'
const Lists = {
data () {
@@ -8,11 +7,7 @@ const Lists = {
}
},
components: {
- ListsCard,
- ListsNew
- },
- created () {
- this.$store.dispatch('startFetchingLists')
+ ListsCard
},
computed: {
lists () {
diff --git a/src/components/lists/lists.vue b/src/components/lists/lists.vue
index fcc56447..b8bab0a0 100644
--- a/src/components/lists/lists.vue
+++ b/src/components/lists/lists.vue
@@ -1,21 +1,15 @@
<template>
- <div v-if="isNew">
- <ListsNew @cancel="cancelNewList" />
- </div>
- <div
- v-else
- class="settings panel panel-default"
- >
+ <div class="Lists panel panel-default">
<div class="panel-heading">
<div class="title">
{{ $t('lists.lists') }}
</div>
- <button
- class="button-default"
- @click="newList"
+ <router-link
+ :to="{ name: 'lists-new' }"
+ class="button-default btn new-list-button"
>
{{ $t("lists.new") }}
- </button>
+ </router-link>
</div>
<div class="panel-body">
<ListsCard
@@ -29,3 +23,11 @@
</template>
<script src="./lists.js"></script>
+
+<style lang="scss">
+.Lists {
+ .new-list-button {
+ padding: 0 0.5em;
+ }
+}
+</style>
diff --git a/src/components/lists_edit/lists_edit.js b/src/components/lists_edit/lists_edit.js
index a68bb589..c22d1323 100644
--- a/src/components/lists_edit/lists_edit.js
+++ b/src/components/lists_edit/lists_edit.js
@@ -1,7 +1,9 @@
import { mapState, mapGetters } from 'vuex'
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import ListsUserSearch from '../lists_user_search/lists_user_search.vue'
+import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faSearch,
@@ -17,22 +19,33 @@ const ListsNew = {
components: {
BasicUserCard,
UserAvatar,
- ListsUserSearch
+ ListsUserSearch,
+ TabSwitcher,
+ PanelLoading
},
data () {
return {
title: '',
- userIds: [],
- selectedUserIds: []
+ titleDraft: '',
+ membersUserIds: [],
+ removedUserIds: new Set([]), // users we added for members, to undo
+ searchUserIds: [],
+ addedUserIds: new Set([]), // users we added from search, to undo
+ searchLoading: false,
+ reallyDelete: false
}
},
created () {
- this.$store.dispatch('fetchList', { id: this.id })
- .then(() => { this.title = this.findListTitle(this.id) })
- this.$store.dispatch('fetchListAccounts', { id: this.id })
+ if (!this.id) return
+ this.$store.dispatch('fetchList', { listId: this.id })
.then(() => {
- this.selectedUserIds = this.findListAccounts(this.id)
- this.selectedUserIds.forEach(userId => {
+ this.title = this.findListTitle(this.id)
+ this.titleDraft = this.title
+ })
+ this.$store.dispatch('fetchListAccounts', { listId: this.id })
+ .then(() => {
+ this.membersUserIds = this.findListAccounts(this.id)
+ this.membersUserIds.forEach(userId => {
this.$store.dispatch('fetchUserIfMissing', userId)
})
})
@@ -41,11 +54,12 @@ const ListsNew = {
id () {
return this.$route.params.id
},
- users () {
- return this.userIds.map(userId => this.findUser(userId))
+ membersUsers () {
+ return [...this.membersUserIds, ...this.addedUserIds]
+ .map(userId => this.findUser(userId)).filter(user => user)
},
- selectedUsers () {
- return this.selectedUserIds.map(userId => this.findUser(userId)).filter(user => user)
+ searchUsers () {
+ return this.searchUserIds.map(userId => this.findUser(userId)).filter(user => user)
},
...mapState({
currentUser: state => state.users.currentUser
@@ -56,33 +70,73 @@ const ListsNew = {
onInput () {
this.search(this.query)
},
- selectUser (user) {
- if (this.selectedUserIds.includes(user.id)) {
- this.removeUser(user.id)
+ toggleRemoveMember (user) {
+ if (this.removedUserIds.has(user.id)) {
+ this.id && this.addUser(user)
+ this.removedUserIds.delete(user.id)
} else {
- this.addUser(user)
+ this.id && this.removeUser(user.id)
+ this.removedUserIds.add(user.id)
}
},
- isSelected (user) {
- return this.selectedUserIds.includes(user.id)
+ toggleAddFromSearch (user) {
+ if (this.addedUserIds.has(user.id)) {
+ this.id && this.removeUser(user.id)
+ this.addedUserIds.delete(user.id)
+ } else {
+ this.id && this.addUser(user)
+ this.addedUserIds.add(user.id)
+ }
+ },
+ isRemoved (user) {
+ return this.removedUserIds.has(user.id)
+ },
+ isAdded (user) {
+ return this.addedUserIds.has(user.id)
},
addUser (user) {
- this.selectedUserIds.push(user.id)
+ this.$store.dispatch('addListAccount', { accountId: this.user.id, listId: this.id })
},
removeUser (userId) {
- this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId)
+ this.$store.dispatch('removeListAccount', { accountId: this.user.id, listId: this.id })
},
- onResults (results) {
- this.userIds = results
+ onSearchLoading (results) {
+ this.searchLoading = true
},
- updateList () {
- this.$store.dispatch('setList', { id: this.id, title: this.title })
- this.$store.dispatch('setListAccounts', { id: this.id, accountIds: this.selectedUserIds })
-
- this.$router.push({ name: 'lists-timeline', params: { id: this.id } })
+ onSearchLoadingDone (results) {
+ this.searchLoading = false
+ },
+ onSearchResults (results) {
+ this.searchLoading = false
+ this.searchUserIds = results
+ },
+ updateListTitle () {
+ this.$store.dispatch('setList', { listId: this.id, title: this.titleDraft })
+ .then(() => {
+ this.title = this.findListTitle(this.id)
+ })
+ },
+ createList () {
+ this.$store.dispatch('createList', { title: this.titleDraft })
+ .then((list) => {
+ return this
+ .$store
+ .dispatch('setListAccounts', { listId: list.id, accountIds: [...this.addedUserIds] })
+ .then(() => list.id)
+ })
+ .then((listId) => {
+ this.$router.push({ name: 'lists-timeline', params: { id: listId } })
+ })
+ .catch((e) => {
+ this.$store.dispatch('pushGlobalNotice', {
+ messageKey: 'lists.error',
+ messageArgs: [e.message],
+ level: 'error'
+ })
+ })
},
deleteList () {
- this.$store.dispatch('deleteList', { id: this.id })
+ this.$store.dispatch('deleteList', { listId: this.id })
this.$router.push({ name: 'lists' })
}
}
diff --git a/src/components/lists_edit/lists_edit.vue b/src/components/lists_edit/lists_edit.vue
index 69007b02..6521aba6 100644
--- a/src/components/lists_edit/lists_edit.vue
+++ b/src/components/lists_edit/lists_edit.vue
@@ -1,8 +1,8 @@
<template>
- <div class="panel-default panel list-edit">
+ <div class="panel-default panel ListEdit">
<div
ref="header"
- class="panel-heading"
+ class="panel-heading list-edit-heading"
>
<button
class="button-unstyled go-back-button"
@@ -13,54 +13,151 @@
icon="chevron-left"
/>
</button>
+ <div class="title">
+ <i18n-t
+ v-if="id"
+ keypath="lists.editing_list"
+ >
+ <template #listTitle>
+ {{ title }}
+ </template>
+ </i18n-t>
+ <i18n-t
+ v-else
+ keypath="lists.creating_list"
+ />
+ </div>
</div>
- <div class="input-wrap">
- <input
- ref="title"
- v-model="title"
- :placeholder="$t('lists.title')"
+ <div class="panel-body">
+ <div class="input-wrap">
+ <label for="list-edit-title">{{ $t('lists.title') }}</label>
+ {{ ' ' }}
+ <input
+ id="list-edit-title"
+ ref="title"
+ v-model="titleDraft"
+ >
+ <button
+ v-if="id"
+ class="btn button-default follow-button"
+ @click="updateListTitle"
+ >
+ {{ $t('lists.update_title') }}
+ </button>
+ </div>
+ <tab-switcher
+ class="list-member-management"
+ :scrollable-tabs="true"
>
+ <div
+ v-if="id || addedUserIds.size > 0"
+ :label="$t('lists.manage_members')"
+ class="members-list"
+ >
+ <div class="users-list">
+ <div
+ v-for="user in membersUsers"
+ :key="user.id"
+ class="member"
+ >
+ <BasicUserCard
+ :user="user"
+ >
+ <button
+ class="btn button-default follow-button"
+ @click="toggleRemoveMember(user)"
+ >
+ {{ isRemoved(user) ? $t('general.undo') : $t('lists.remove_from_list') }}
+ </button>
+ </BasicUserCard>
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="search-list"
+ :label="$t('lists.add_members')"
+ >
+ <ListsUserSearch
+ @results="onSearchResults"
+ @loading="onSearchLoading"
+ @loadingDone="onSearchLoadingDone"
+ />
+ <div
+ v-if="searchLoading"
+ class="loading"
+ >
+ <PanelLoading />
+ </div>
+ <div
+ v-else
+ class="users-list"
+ >
+ <div
+ v-for="user in searchUsers"
+ :key="user.id"
+ class="member"
+ >
+ <BasicUserCard
+ :user="user"
+ >
+ <span
+ v-if="membersUserIds.includes(user.id)"
+ >
+ {{ $t('lists.is_in_list') }}
+ </span>
+ <button
+ v-if="!membersUserIds.includes(user.id)"
+ class="btn button-default follow-button"
+ @click="toggleAddFromSearch(user)"
+ >
+ {{ isAdded(user) ? $t('general.undo') : $t('lists.add_to_list') }}
+ </button>
+ <button
+ v-else
+ class="btn button-default follow-button"
+ @click="toggleRemoveMember(user)"
+ >
+ {{ isRemoved(user) ? $t('general.undo') : $t('lists.remove_from_list') }}
+ </button>
+ </BasicUserCard>
+ </div>
+ </div>
+ </div>
+ </tab-switcher>
</div>
- <div class="member-list">
- <div
- v-for="user in selectedUsers"
- :key="user.id"
- class="member"
+ <div class="panel-footer">
+ <span class="spacer" />
+ <button
+ v-if="!id"
+ class="btn button-default footer-button"
+ @click="createList"
>
- <BasicUserCard
- :user="user"
- :class="isSelected(user) ? 'selected' : ''"
- @click.capture.prevent="selectUser(user)"
- />
- </div>
- </div>
- <ListsUserSearch @results="onResults" />
- <div class="member-list">
- <div
- v-for="user in users"
- :key="user.id"
- class="member"
+ {{ $t('lists.create') }}
+ </button>
+ <button
+ v-else-if="!reallyDelete"
+ class="btn button-default footer-button"
+ @click="reallyDelete = true"
>
- <BasicUserCard
- :user="user"
- :class="isSelected(user) ? 'selected' : ''"
- @click.capture.prevent="selectUser(user)"
- />
- </div>
+ {{ $t('lists.delete') }}
+ </button>
+ <template v-else>
+ {{ $t('lists.really_delete') }}
+ <button
+ class="btn button-default footer-button"
+ @click="deleteList"
+ >
+ {{ $t('general.yes') }}
+ </button>
+ <button
+ class="btn button-default footer-button"
+ @click="reallyDelete = false"
+ >
+ {{ $t('general.no') }}
+ </button>
+ </template>
</div>
- <button
- :disabled="title && title.length === 0"
- class="btn button-default"
- @click="updateList"
- >
- {{ $t('lists.save') }}
- </button>
- <button
- class="btn button-default"
- @click="deleteList"
- >
- {{ $t('lists.delete') }}
- </button>
</div>
</template>
@@ -69,28 +166,43 @@
<style lang="scss">
@import '../../_variables.scss';
-.list-edit {
- .input-wrap {
+.ListEdit {
+ --panel-body-padding: 0.5em;
+
+ height: calc(100vh - var(--navbar-height));
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+
+ .list-edit-heading {
+ grid-template-columns: auto minmax(50%, 1fr);
+ }
+
+ .panel-body {
display: flex;
- margin: 0.7em 0.5em 0.7em 0.5em;
+ flex: 1;
+ flex-direction: column;
+ overflow: hidden;
+ }
- input {
- width: 100%;
- }
+ .list-member-management {
+ flex: 1 0 auto;
}
.search-icon {
margin-right: 0.3em;
}
- .member-list {
+ .users-list {
padding-bottom: 0.7rem;
+ overflow-y: auto;
}
- .basic-user-card:hover,
- .basic-user-card.selected {
- cursor: pointer;
- background-color: var(--selectedPost, $fallback--lightBg);
+ & .search-list,
+ & .members-list {
+ overflow: hidden;
+ flex-direction: column;
+ min-height: 0;
}
.go-back-button {
@@ -102,7 +214,15 @@
}
.btn {
- margin: 0.5em;
+ margin: 0 0.5em;
+ }
+
+ .panel-footer {
+ grid-template-columns: minmax(10%, 1fr);
+
+ .footer-button {
+ min-width: 9em;
+ }
}
}
</style>
diff --git a/src/components/lists_menu/lists_menu_content.js b/src/components/lists_menu/lists_menu_content.js
index 37e7868c..97b32210 100644
--- a/src/components/lists_menu/lists_menu_content.js
+++ b/src/components/lists_menu/lists_menu_content.js
@@ -1,28 +1,17 @@
import { mapState } from 'vuex'
-import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faUsers,
- faGlobe,
- faBookmark,
- faEnvelope,
- faHome
-} from '@fortawesome/free-solid-svg-icons'
+import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
+import { getListEntries } from 'src/components/navigation/filter.js'
-library.add(
- faUsers,
- faGlobe,
- faBookmark,
- faEnvelope,
- faHome
-)
-
-const ListsMenuContent = {
- created () {
- this.$store.dispatch('startFetchingLists')
+export const ListsMenuContent = {
+ props: [
+ 'showPin'
+ ],
+ components: {
+ NavigationEntry
},
computed: {
...mapState({
- lists: state => state.lists.allLists,
+ lists: getListEntries,
currentUser: state => state.users.currentUser,
privateMode: state => state.instance.private,
federating: state => state.instance.federating
diff --git a/src/components/lists_menu/lists_menu_content.vue b/src/components/lists_menu/lists_menu_content.vue
index e910d6eb..f93e80c9 100644
--- a/src/components/lists_menu/lists_menu_content.vue
+++ b/src/components/lists_menu/lists_menu_content.vue
@@ -1,16 +1,11 @@
<template>
<ul>
- <li
- v-for="list in lists.slice().reverse()"
- :key="list.id"
- >
- <router-link
- class="menu-item"
- :to="{ name: 'lists-timeline', params: { id: list.id } }"
- >
- {{ list.title }}
- </router-link>
- </li>
+ <NavigationEntry
+ v-for="item in lists"
+ :key="item.name"
+ :show-pin="showPin"
+ :item="item"
+ />
</ul>
</template>
diff --git a/src/components/lists_new/lists_new.js b/src/components/lists_new/lists_new.js
deleted file mode 100644
index 63dc28ad..00000000
--- a/src/components/lists_new/lists_new.js
+++ /dev/null
@@ -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
diff --git a/src/components/lists_new/lists_new.vue b/src/components/lists_new/lists_new.vue
deleted file mode 100644
index 4733bdde..00000000
--- a/src/components/lists_new/lists_new.vue
+++ /dev/null
@@ -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>
diff --git a/src/components/lists_timeline/lists_timeline.js b/src/components/lists_timeline/lists_timeline.js
index 7534420d..c3f408bd 100644
--- a/src/components/lists_timeline/lists_timeline.js
+++ b/src/components/lists_timeline/lists_timeline.js
@@ -17,14 +17,14 @@ const ListsTimeline = {
this.listId = route.params.id
this.$store.dispatch('stopFetchingTimeline', 'list')
this.$store.commit('clearTimeline', { timeline: 'list' })
- this.$store.dispatch('fetchList', { id: this.listId })
+ this.$store.dispatch('fetchList', { listId: this.listId })
this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId })
}
}
},
created () {
this.listId = this.$route.params.id
- this.$store.dispatch('fetchList', { id: this.listId })
+ this.$store.dispatch('fetchList', { listId: this.listId })
this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId })
},
unmounted () {
diff --git a/src/components/lists_user_search/lists_user_search.js b/src/components/lists_user_search/lists_user_search.js
index 5798841a..c92ec0ee 100644
--- a/src/components/lists_user_search/lists_user_search.js
+++ b/src/components/lists_user_search/lists_user_search.js
@@ -15,6 +15,7 @@ const ListsUserSearch = {
components: {
Checkbox
},
+ emits: ['loading', 'loadingDone', 'results'],
data () {
return {
loading: false,
@@ -33,12 +34,16 @@ const ListsUserSearch = {
}
this.loading = true
+ this.$emit('loading')
this.userIds = []
this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts', following: this.followingOnly })
.then(data => {
- this.loading = false
this.$emit('results', data.accounts.map(a => a.id))
})
+ .finally(() => {
+ this.loading = false
+ this.$emit('loadingDone')
+ })
}
}
}
diff --git a/src/components/lists_user_search/lists_user_search.vue b/src/components/lists_user_search/lists_user_search.vue
index 03fb3ba6..8633170c 100644
--- a/src/components/lists_user_search/lists_user_search.vue
+++ b/src/components/lists_user_search/lists_user_search.vue
@@ -1,5 +1,5 @@
<template>
- <div>
+ <div class="ListsUserSearch">
<div class="input-wrap">
<div class="input-search">
<FAIcon
@@ -29,17 +29,19 @@
<style lang="scss">
@import '../../_variables.scss';
-.input-wrap {
- display: flex;
- margin: 0.7em 0.5em 0.7em 0.5em;
+.ListsUserSearch {
+ .input-wrap {
+ display: flex;
+ margin: 0.7em 0.5em 0.7em 0.5em;
- input {
- width: 100%;
+ input {
+ width: 100%;
+ }
}
-}
-.search-icon {
- margin-right: 0.3em;
+ .search-icon {
+ margin-right: 0.3em;
+ }
}
</style>
diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index 4a74fbe2..6515bd11 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -2,6 +2,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
import { mapGetters, mapState } from 'vuex'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import UserAvatar from '../user_avatar/user_avatar.vue'
+import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import { defineAsyncComponent } from 'vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@@ -16,6 +17,7 @@ const MentionLink = {
name: 'MentionLink',
components: {
UserAvatar,
+ UnicodeDomainIndicator,
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
},
props: {
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 3af502ef..869a3257 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -47,6 +47,9 @@
class="serverName"
:class="{ '-faded': shouldFadeDomain }"
v-html="'@' + serverName"
+ /><UnicodeDomainIndicator
+ v-if="shouldShowFullUserName"
+ :user="user"
/>
</span>
<span
diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js
index 3b41d2ce..88cbe1be 100644
--- a/src/components/mobile_nav/mobile_nav.js
+++ b/src/components/mobile_nav/mobile_nav.js
@@ -2,6 +2,7 @@ import SideDrawer from '../side_drawer/side_drawer.vue'
import Notifications from '../notifications/notifications.vue'
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
import GestureService from '../../services/gesture_service/gesture_service'
+import NavigationPins from 'src/components/navigation/navigation_pins.vue'
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@@ -19,7 +20,8 @@ library.add(
const MobileNav = {
components: {
SideDrawer,
- Notifications
+ Notifications,
+ NavigationPins
},
data: () => ({
notificationsCloseGesture: undefined,
@@ -47,7 +49,10 @@ const MobileNav = {
isChat () {
return this.$route.name === 'chat'
},
- ...mapGetters(['unreadChatCount'])
+ ...mapGetters(['unreadChatCount']),
+ chatsPinned () {
+ return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats')
+ }
},
methods: {
toggleMobileSidebar () {
diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue
index e3459681..c38be75b 100644
--- a/src/components/mobile_nav/mobile_nav.vue
+++ b/src/components/mobile_nav/mobile_nav.vue
@@ -17,20 +17,12 @@
icon="bars"
/>
<div
- v-if="unreadChatCount"
+ v-if="unreadChatCount && !chatsPinned"
class="alert-dot"
/>
</button>
- <router-link
- v-if="!hideSitename"
- class="site-name"
- :to="{ name: 'root' }"
- active-class="home"
- >
- {{ sitename }}
- </router-link>
- </div>
- <div class="item right">
+ <NavigationPins class="pins" />
+ </div> <div class="item right">
<button
v-if="currentUser"
class="button-unstyled mobile-nav-button"
@@ -94,6 +86,7 @@
grid-template-columns: 2fr auto;
width: 100%;
box-sizing: border-box;
+
a {
color: var(--topBarLink, $fallback--link);
}
@@ -178,13 +171,20 @@
}
}
+ .pins {
+ flex: 1;
+
+ .pinned-item {
+ flex-grow: 1;
+ }
+ }
+
.mobile-notifications {
margin-top: 50px;
width: 100vw;
height: calc(100vh - var(--navbar-height));
overflow-x: hidden;
overflow-y: scroll;
-
color: $fallback--text;
color: var(--text, $fallback--text);
background-color: $fallback--bg;
@@ -194,14 +194,17 @@
padding: 0;
border-radius: 0;
box-shadow: none;
+
.panel {
border-radius: 0;
margin: 0;
box-shadow: none;
}
- .panel:after {
+
+ .panel::after {
border-radius: 0;
}
+
.panel .panel-heading {
border-radius: 0;
box-shadow: none;
diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js
index ecf79b64..f7f96cd6 100644
--- a/src/components/mobile_post_status_button/mobile_post_status_button.js
+++ b/src/components/mobile_post_status_button/mobile_post_status_button.js
@@ -10,7 +10,8 @@ library.add(
const HIDDEN_FOR_PAGES = new Set([
'chats',
- 'chat'
+ 'chat',
+ 'lists-edit'
])
const MobilePostStatusButton = {
diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js
index abeff6bf..b54f2fa2 100644
--- a/src/components/nav_panel/nav_panel.js
+++ b/src/components/nav_panel/nav_panel.js
@@ -1,6 +1,10 @@
-import TimelineMenuContent from '../timeline_menu/timeline_menu_content.vue'
-import ListsMenuContent from '../lists_menu/lists_menu_content.vue'
+import ListsMenuContent from 'src/components/lists_menu/lists_menu_content.vue'
import { mapState, mapGetters } from 'vuex'
+import { TIMELINES, ROOT_ITEMS } from 'src/components/navigation/navigation.js'
+import { filterNavigation } from 'src/components/navigation/filter.js'
+import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
+import NavigationPins from 'src/components/navigation/navigation_pins.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@@ -30,21 +34,23 @@ library.add(
faStream,
faList
)
-
const NavPanel = {
+ props: ['forceExpand', 'forceEditMode'],
created () {
- if (this.currentUser && this.currentUser.locked) {
- this.$store.dispatch('startFetchingFollowRequests')
- }
},
components: {
- TimelineMenuContent,
- ListsMenuContent
+ ListsMenuContent,
+ NavigationEntry,
+ NavigationPins,
+ Checkbox
},
data () {
return {
+ editMode: false,
showTimelines: false,
- showLists: false
+ showLists: false,
+ timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k })),
+ rootList: Object.entries(ROOT_ITEMS).map(([k, v]) => ({ ...v, name: k }))
}
},
methods: {
@@ -53,19 +59,62 @@ const NavPanel = {
},
toggleLists () {
this.showLists = !this.showLists
+ },
+ toggleEditMode () {
+ this.editMode = !this.editMode
+ },
+ toggleCollapse () {
+ this.$store.commit('setPreference', { path: 'simple.collapseNav', value: !this.collapsed })
+ this.$store.dispatch('pushServerSideStorage')
+ },
+ isPinned (item) {
+ return this.pinnedItems.has(item)
+ },
+ togglePin (item) {
+ if (this.isPinned(item)) {
+ this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value: item })
+ } else {
+ this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value: item })
+ }
+ this.$store.dispatch('pushServerSideStorage')
}
},
computed: {
- listsNavigation () {
- return this.$store.getters.mergedConfig.listsNavigation
- },
...mapState({
currentUser: state => state.users.currentUser,
followRequestCount: state => state.api.followRequests.length,
privateMode: state => state.instance.private,
federating: state => state.instance.federating,
- pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
+ pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
+ pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems),
+ collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav
}),
+ timelinesItems () {
+ return filterNavigation(
+ Object
+ .entries({ ...TIMELINES })
+ .map(([k, v]) => ({ ...v, name: k })),
+ {
+ hasChats: this.pleromaChatMessagesAvailable,
+ isFederating: this.federating,
+ isPrivate: this.privateMode,
+ currentUser: this.currentUser
+ }
+ )
+ },
+ rootItems () {
+ return filterNavigation(
+ Object
+ .entries({ ...ROOT_ITEMS })
+ .map(([k, v]) => ({ ...v, name: k })),
+ {
+ hasChats: this.pleromaChatMessagesAvailable,
+ isFederating: this.federating,
+ isPrivate: this.privateMode,
+ currentUser: this.currentUser
+ }
+ )
+ },
...mapGetters(['unreadChatCount'])
}
}
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index 9322e233..2688bcf4 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -1,135 +1,99 @@
<template>
<div class="NavPanel">
<div class="panel panel-default">
- <ul>
- <li v-if="currentUser || !privateMode">
- <button
- class="button-unstyled menu-item"
- @click="toggleTimelines"
- >
- <FAIcon
- fixed-width
- class="fa-scale-110"
- icon="stream"
- />{{ $t("nav.timelines") }}
- <FAIcon
- class="timelines-chevron"
- fixed-width
- :icon="showTimelines ? 'chevron-up' : 'chevron-down'"
- />
- </button>
- <div
- v-show="showTimelines"
- class="timelines-background"
- >
- <TimelineMenuContent class="timelines" />
- </div>
- </li>
- <li v-if="currentUser && listsNavigation">
- <button
- class="button-unstyled menu-item"
- @click="toggleLists"
- >
- <router-link
- :to="{ name: 'lists' }"
- @click.stop
- >
- <FAIcon
- fixed-width
- class="fa-scale-110"
- icon="list"
- />{{ $t("nav.lists") }}
- </router-link>
- <FAIcon
- class="timelines-chevron"
- fixed-width
- :icon="showLists ? 'chevron-up' : 'chevron-down'"
+ <div
+ v-if="!forceExpand"
+ class="panel-heading nav-panel-heading"
+ >
+ <NavigationPins :limit="6" />
+ <div class="spacer" />
+ <button
+ class="button-unstyled"
+ @click="toggleCollapse"
+ >
+ <FAIcon
+ class="timelines-chevron"
+ fixed-width
+ :icon="collapsed ? 'chevron-down' : 'chevron-up'"
+ />
+ </button>
+ </div>
+ <ul
+ v-if="!collapsed || forceExpand"
+ class="panel-body"
+ >
+ <NavigationEntry
+ v-if="currentUser || !privateMode"
+ :show-pin="false"
+ :item="{ icon: 'stream', label: 'nav.timelines' }"
+ :aria-expanded="showTimelines ? 'true' : 'false'"
+ @click="toggleTimelines"
+ >
+ <FAIcon
+ class="timelines-chevron"
+ fixed-width
+ :icon="showTimelines ? 'chevron-up' : 'chevron-down'"
+ />
+ </NavigationEntry>
+ <div
+ v-show="showTimelines"
+ class="timelines-background"
+ >
+ <div class="timelines">
+ <NavigationEntry
+ v-for="item in timelinesItems"
+ :key="item.name"
+ :show-pin="editMode || forceEditMode"
+ :item="item"
/>
- </button>
- <div
- v-show="showLists"
- class="timelines-background"
- >
- <ListsMenuContent class="timelines" />
</div>
- </li>
- <li v-if="currentUser && !listsNavigation">
+ </div>
+ <NavigationEntry
+ v-if="currentUser"
+ :show-pin="false"
+ :item="{ icon: 'list', label: 'nav.lists' }"
+ :aria-expanded="showLists ? 'true' : 'false'"
+ @click="toggleLists"
+ >
<router-link
+ :title="$t('lists.manage_lists')"
+ class="extra-button"
:to="{ name: 'lists' }"
@click.stop
>
- <button
- class="button-unstyled menu-item"
- @click="toggleLists"
- >
- <FAIcon
- fixed-width
- class="fa-scale-110"
- icon="list"
- />{{ $t("nav.lists") }}
- </button>
- </router-link>
- </li>
- <li v-if="currentUser">
- <router-link
- class="menu-item"
- :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"
- >
- <FAIcon
- fixed-width
- class="fa-scale-110"
- icon="bell"
- />{{ $t("nav.interactions") }}
- </router-link>
- </li>
- <li v-if="currentUser && pleromaChatMessagesAvailable">
- <router-link
- class="menu-item"
- :to="{ name: 'chats', params: { username: currentUser.screen_name } }"
- >
- <div
- v-if="unreadChatCount"
- class="badge badge-notification"
- >
- {{ unreadChatCount }}
- </div>
- <FAIcon
- fixed-width
- class="fa-scale-110"
- icon="comments"
- />{{ $t("nav.chats") }}
- </router-link>
- </li>
- <li v-if="currentUser && currentUser.locked">
- <router-link
- class="menu-item"
- :to="{ name: 'friend-requests' }"
- >
- <FAIcon
- fixed-width
- class="fa-scale-110"
- icon="user-plus"
- />{{ $t("nav.friend_requests") }}
- <span
- v-if="followRequestCount > 0"
- class="badge badge-notification"
- >
- {{ followRequestCount }}
- </span>
- </router-link>
- </li>
- <li>
- <router-link
- class="menu-item"
- :to="{ name: 'about' }"
- >
<FAIcon
+ class="extra-button"
fixed-width
- class="fa-scale-110"
- icon="info-circle"
- />{{ $t("nav.about") }}
+ icon="wrench"
+ />
</router-link>
- </li>
+ <FAIcon
+ class="timelines-chevron"
+ fixed-width
+ :icon="showLists ? 'chevron-up' : 'chevron-down'"
+ />
+ </NavigationEntry>
+ <div
+ v-show="showLists"
+ class="timelines-background"
+ >
+ <ListsMenuContent
+ :show-pin="editMode || forceEditMode"
+ class="timelines"
+ />
+ </div>
+ <NavigationEntry
+ v-for="item in rootItems"
+ :key="item.name"
+ :show-pin="editMode || forceEditMode"
+ :item="item"
+ />
+ <NavigationEntry
+ v-if="!forceEditMode && currentUser"
+ :show-pin="false"
+ :item="{ label: editMode ? $t('nav.edit_finish') : $t('nav.edit_pinned'), icon: editMode ? 'check' : 'wrench' }"
+ @click="toggleEditMode"
+ />
</ul>
</div>
</div>
@@ -180,54 +144,23 @@
border: none;
}
- .menu-item {
- display: block;
- box-sizing: border-box;
- height: 3.5em;
- line-height: 3.5em;
- padding: 0 1em;
- width: 100%;
- color: $fallback--link;
- color: var(--link, $fallback--link);
-
- &:hover {
- background-color: $fallback--lightBg;
- background-color: var(--selectedMenu, $fallback--lightBg);
- color: $fallback--link;
- color: var(--selectedMenuText, $fallback--link);
- --faint: var(--selectedMenuFaintText, $fallback--faint);
- --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
- --lightText: var(--selectedMenuLightText, $fallback--lightText);
- --icon: var(--selectedMenuIcon, $fallback--icon);
- }
-
- &.router-link-active {
- font-weight: bolder;
- background-color: $fallback--lightBg;
- background-color: var(--selectedMenu, $fallback--lightBg);
- color: $fallback--text;
- color: var(--selectedMenuText, $fallback--text);
- --faint: var(--selectedMenuFaintText, $fallback--faint);
- --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
- --lightText: var(--selectedMenuLightText, $fallback--lightText);
- --icon: var(--selectedMenuIcon, $fallback--icon);
-
- &:hover {
- text-decoration: underline;
- }
- }
- }
-
.timelines-chevron {
margin-left: 0.8em;
+ margin-right: 0.8em;
font-size: 1.1em;
}
+ .menu-item {
+ .timelines-chevron {
+ margin-right: 0;
+ }
+ }
+
.timelines-background {
padding: 0 0 0 0.6em;
background-color: $fallback--lightBg;
background-color: var(--selectedMenu, $fallback--lightBg);
- border-top: 1px solid;
+ border-bottom: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
}
@@ -237,14 +170,9 @@
background-color: var(--bg, $fallback--bg);
}
- .fa-scale-110 {
- margin-right: 0.8em;
- }
-
- .badge {
- position: absolute;
- right: 0.6rem;
- top: 1.25em;
+ .nav-panel-heading {
+ // breaks without a unit
+ --panel-heading-height-padding: 0em;
}
}
</style>
diff --git a/src/components/navigation/filter.js b/src/components/navigation/filter.js
new file mode 100644
index 00000000..31b55486
--- /dev/null
+++ b/src/components/navigation/filter.js
@@ -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]
+}))
diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js
new file mode 100644
index 00000000..f66dd981
--- /dev/null
+++ b/src/components/navigation/navigation.js
@@ -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'
+ }
+}
diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js
new file mode 100644
index 00000000..fe3402fc
--- /dev/null
+++ b/src/components/navigation/navigation_entry.js
@@ -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
diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue
new file mode 100644
index 00000000..824c00a2
--- /dev/null
+++ b/src/components/navigation/navigation_entry.vue
@@ -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>
diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js
new file mode 100644
index 00000000..57b8d589
--- /dev/null
+++ b/src/components/navigation/navigation_pins.js
@@ -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
diff --git a/src/components/navigation/navigation_pins.vue b/src/components/navigation/navigation_pins.vue
new file mode 100644
index 00000000..5b3fa6f4
--- /dev/null
+++ b/src/components/navigation/navigation_pins.vue
@@ -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>
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index f60a1a6a..ddba560e 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -5,6 +5,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue'
import Report from '../report/report.vue'
+import UserLink from '../user_link/user_link.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import UserPopover from '../user_popover/user_popover.vue'
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
@@ -50,7 +51,8 @@ const Notification = {
Status,
Report,
RichContent,
- UserPopover
+ UserPopover,
+ UserLink
},
methods: {
toggleUserExpanded () {
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 6a5417b3..26b174ff 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -11,9 +11,10 @@
class="Notification container -muted"
>
<small>
- <router-link :to="userProfileLink">
- {{ notification.from_profile.screen_name_ui }}
- </router-link>
+ <user-link
+ :user="notification.from_profile"
+ :at="false"
+ />
</small>
<button
class="button-unstyled unmute"
@@ -174,12 +175,10 @@
v-if="notification.type === 'follow' || notification.type === 'follow_request'"
class="follow-text"
>
- <router-link
- :to="userProfileLink"
+ <user-link
class="follow-name"
- >
- @{{ notification.from_profile.screen_name_ui }}
- </router-link>
+ :user="notification.from_profile"
+ />
<div
v-if="notification.type === 'follow_request'"
style="white-space: nowrap;"
@@ -210,9 +209,9 @@
v-else-if="notification.type === 'move'"
class="move-text"
>
- <router-link :to="targetUserProfileLink">
- @{{ notification.target.screen_name_ui }}
- </router-link>
+ <user-link
+ :user="notification.target"
+ />
</div>
<Report
v-else-if="notification.type === 'pleroma:report'"
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index d2af59fe..dd332c35 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -4,7 +4,7 @@ const Popover = {
// Action to trigger popover: either 'hover' or 'click'
trigger: String,
- // Either 'top' or 'bottom'
+ // 'top', 'bottom', 'left', 'right'
placement: String,
// Takes object with properties 'x' and 'y', values of these can be
@@ -84,6 +84,8 @@ const Popover = {
const anchorStyle = getComputedStyle(anchorEl)
const topPadding = parseFloat(anchorStyle.paddingTop)
const bottomPadding = parseFloat(anchorStyle.paddingBottom)
+ const rightPadding = parseFloat(anchorStyle.paddingRight)
+ const leftPadding = parseFloat(anchorStyle.paddingLeft)
// Screen position of the origin point for popover = center of the anchor
const origin = {
@@ -170,7 +172,7 @@ const Popover = {
if (overlayCenter) {
translateX = origin.x + horizOffset
translateY = origin.y + vertOffset
- } else {
+ } else if (this.placement !== 'right' && this.placement !== 'left') {
// Default to whatever user wished with placement prop
let usingTop = this.placement !== 'bottom'
@@ -189,6 +191,25 @@ const Popover = {
const xOffset = (this.offset && this.offset.x) || 0
translateX = origin.x + horizOffset + xOffset
+ } else {
+ // Default to whatever user wished with placement prop
+ let usingRight = this.placement !== 'left'
+
+ // Handle special cases, first force to displaying on top if there's not space on bottom,
+ // regardless of what placement value was. Then check if there's not space on top, and
+ // force to bottom, again regardless of what placement value was.
+ const rightBoundary = origin.x - anchorWidth * 0.5 + (this.removePadding ? rightPadding : 0)
+ const leftBoundary = origin.x + anchorWidth * 0.5 - (this.removePadding ? leftPadding : 0)
+ if (leftBoundary + content.offsetWidth > xBounds.max) usingRight = true
+ if (rightBoundary - content.offsetWidth < xBounds.min) usingRight = false
+
+ const xOffset = (this.offset && this.offset.x) || 0
+ translateX = usingRight
+ ? rightBoundary - xOffset - content.offsetWidth
+ : leftBoundary + xOffset
+
+ const yOffset = (this.offset && this.offset.y) || 0
+ translateY = origin.y + vertOffset + yOffset
}
this.styles = {
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
index bd59cade..623af8d2 100644
--- a/src/components/popover/popover.vue
+++ b/src/components/popover/popover.vue
@@ -126,6 +126,13 @@
}
}
+ &.-has-submenu {
+ .chevron-icon {
+ margin-right: 0.25rem;
+ margin-left: 2rem;
+ }
+ }
+
&:active, &:hover {
background-color: $fallback--lightBg;
background-color: var(--selectedMenuPopover, $fallback--lightBg);
diff --git a/src/components/search_bar/search_bar.vue b/src/components/search_bar/search_bar.vue
index 222f57ba..199a7500 100644
--- a/src/components/search_bar/search_bar.vue
+++ b/src/components/search_bar/search_bar.vue
@@ -47,6 +47,8 @@
class="cancel-icon fa-scale-110 fa-old-padding"
/>
</button>
+ <span class="spacer" />
+ <span class="spacer" />
</template>
</div>
</template>
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 50e2bc44..8561647b 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -102,11 +102,6 @@
</BooleanSetting>
</li>
<li>
- <BooleanSetting path="listsNavigation">
- {{ $t('settings.lists_navigation') }}
- </BooleanSetting>
- </li>
- <li>
<h3>{{ $t('settings.columns') }}</h3>
</li>
<li>
diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js
index 913fa695..bb22446b 100644
--- a/src/components/side_drawer/side_drawer.js
+++ b/src/components/side_drawer/side_drawer.js
@@ -2,6 +2,7 @@ import { mapState, mapGetters } from 'vuex'
import UserCard from '../user_card/user_card.vue'
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
import GestureService from '../../services/gesture_service/gesture_service'
+import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faSignInAlt,
@@ -15,6 +16,7 @@ import {
faTachometerAlt,
faCog,
faInfoCircle,
+ faCompass,
faList
} from '@fortawesome/free-solid-svg-icons'
@@ -30,6 +32,7 @@ library.add(
faTachometerAlt,
faCog,
faInfoCircle,
+ faCompass,
faList
)
@@ -80,10 +83,16 @@ const SideDrawer = {
return this.$store.state.instance.federating
},
timelinesRoute () {
+ let name
if (this.$store.state.interface.lastTimeline) {
- return this.$store.state.interface.lastTimeline
+ name = this.$store.state.interface.lastTimeline
+ }
+ name = this.currentUser ? 'friends' : 'public-timeline'
+ if (USERNAME_ROUTES.has(name)) {
+ return { name, params: { username: this.currentUser.screen_name } }
+ } else {
+ return { name }
}
- return this.currentUser ? 'friends' : 'public-timeline'
},
...mapState({
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 7d9d36d7..cbeafdd2 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -47,7 +47,7 @@
v-if="currentUser || !privateMode"
@click="toggleDrawer"
>
- <router-link :to="{ name: timelinesRoute }">
+ <router-link :to="timelinesRoute">
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
@@ -195,6 +195,18 @@
v-if="currentUser"
@click="toggleDrawer"
>
+ <router-link :to="{ name: 'edit-navigation' }">
+ <FAIcon
+ fixed-width
+ class="fa-scale-110 fa-old-padding"
+ icon="compass"
+ /> {{ $t("nav.edit_nav_mobile") }}
+ </router-link>
+ </li>
+ <li
+ v-if="currentUser"
+ @click="toggleDrawer"
+ >
<button
class="button-unstyled -link -fullwidth"
@click="doLogout"
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 384063a7..19356664 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -13,6 +13,7 @@ import StatusPopover from '../status_popover/status_popover.vue'
import UserPopover from '../user_popover/user_popover.vue'
import UserListPopover from '../user_list_popover/user_list_popover.vue'
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
+import UserLink from '../user_link/user_link.vue'
import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
import MentionLink from 'src/components/mention_link/mention_link.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@@ -115,7 +116,8 @@ const Status = {
RichContent,
MentionLink,
MentionsLine,
- UserPopover
+ UserPopover,
+ UserLink
},
props: [
'statusoid',
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 967a966c..91f5fffa 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -25,9 +25,10 @@
class="fa-scale-110 fa-old-padding repeat-icon"
icon="retweet"
/>
- <router-link :to="userProfileLink">
- {{ status.user.screen_name_ui }}
- </router-link>
+ <user-link
+ :user="status.user"
+ :at="false"
+ />
</small>
<small
v-if="showReasonMutedThread"
@@ -164,13 +165,12 @@
>
{{ status.user.name }}
</h4>
- <router-link
+ <user-link
class="account-name"
:title="status.user.screen_name_ui"
- :to="userProfileLink"
- >
- {{ status.user.screen_name_ui }}
- </router-link>
+ :user="status.user"
+ :at="false"
+ />
<img
v-if="!!(status.user && status.user.favicon)"
class="status-favicon"
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index 7a086b26..d930368c 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -17,6 +17,7 @@
overflow-x: auto;
padding-top: 5px;
flex-direction: row;
+ flex: 0 0 auto;
&::after, &::before {
content: '';
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index f9f5bf92..a196d65e 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -1,7 +1,10 @@
<template>
<div :class="['Timeline', classes.root]">
<div :class="classes.header">
- <TimelineMenu v-if="!embedded" />
+ <TimelineMenu
+ v-if="!embedded"
+ :timeline-name="timelineName"
+ />
<button
v-if="showScrollTop"
class="button-unstyled scroll-to-top-button"
diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js
index 5a67b0a5..d74fbf4e 100644
--- a/src/components/timeline_menu/timeline_menu.js
+++ b/src/components/timeline_menu/timeline_menu.js
@@ -1,6 +1,8 @@
import Popover from '../popover/popover.vue'
-import TimelineMenuContent from './timeline_menu_content.vue'
+import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
+import { ListsMenuContent } from '../lists_menu/lists_menu_content.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
+import { TIMELINES } from 'src/components/navigation/navigation.js'
import {
faChevronDown
} from '@fortawesome/free-solid-svg-icons'
@@ -22,11 +24,13 @@ export const timelineNames = () => {
const TimelineMenu = {
components: {
Popover,
- TimelineMenuContent
+ NavigationEntry,
+ ListsMenuContent
},
data () {
return {
- isOpen: false
+ isOpen: false,
+ timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k }))
}
},
created () {
@@ -34,6 +38,12 @@ const TimelineMenu = {
this.$store.dispatch('setLastTimeline', this.$route.name)
}
},
+ computed: {
+ useListsMenu () {
+ const route = this.$route.name
+ return route === 'lists-timeline'
+ }
+ },
methods: {
openMenu () {
// $nextTick is too fast, animation won't play back but
diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue
index c24b9d72..e7250282 100644
--- a/src/components/timeline_menu/timeline_menu.vue
+++ b/src/components/timeline_menu/timeline_menu.vue
@@ -10,7 +10,19 @@
@close="() => isOpen = false"
>
<template #content>
- <TimelineMenuContent />
+ <ListsMenuContent
+ v-if="useListsMenu"
+ :show-pin="false"
+ class="timelines"
+ />
+ <ul v-else>
+ <NavigationEntry
+ v-for="item in timelinesList"
+ :key="item.name"
+ :show-pin="false"
+ :item="item"
+ />
+ </ul>
</template>
<template #trigger>
<span class="button-unstyled title timeline-menu-title">
@@ -138,8 +150,7 @@
background-color: $fallback--lightBg;
background-color: var(--selectedMenu, $fallback--lightBg);
color: $fallback--text;
- color: var(--selectedMenuText, $fallback--text);
- --faint: var(--selectedMenuFaintText, $fallback--faint);
+ color: var(--selectedMenuText, $fallback--text); --faint: var(--selectedMenuFaintText, $fallback--faint);
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
--lightText: var(--selectedMenuLightText, $fallback--lightText);
--icon: var(--selectedMenuIcon, $fallback--icon);
diff --git a/src/components/timeline_menu/timeline_menu_content.js b/src/components/timeline_menu/timeline_menu_content.js
deleted file mode 100644
index 671570dd..00000000
--- a/src/components/timeline_menu/timeline_menu_content.js
+++ /dev/null
@@ -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
diff --git a/src/components/timeline_menu/timeline_menu_content.vue b/src/components/timeline_menu/timeline_menu_content.vue
deleted file mode 100644
index 59e9e43c..00000000
--- a/src/components/timeline_menu/timeline_menu_content.vue
+++ /dev/null
@@ -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>
diff --git a/src/components/unicode_domain_indicator/unicode_domain_indicator.vue b/src/components/unicode_domain_indicator/unicode_domain_indicator.vue
new file mode 100644
index 00000000..8f35245f
--- /dev/null
+++ b/src/components/unicode_domain_indicator/unicode_domain_indicator.vue
@@ -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>
diff --git a/src/components/update_notification/update_notification.js b/src/components/update_notification/update_notification.js
index ba008d81..609842c4 100644
--- a/src/components/update_notification/update_notification.js
+++ b/src/components/update_notification/update_notification.js
@@ -38,7 +38,7 @@ const UpdateNotification = {
return !this.$store.state.instance.disableUpdateNotification &&
this.$store.state.users.currentUser &&
this.$store.state.serverSideStorage.flagStorage.updateCounter < CURRENT_UPDATE_COUNTER &&
- !this.$store.state.serverSideStorage.flagStorage.dontShowUpdateNotifs
+ !this.$store.state.serverSideStorage.prefsStorage.simple.dontShowUpdateNotifs
}
},
methods: {
@@ -48,7 +48,7 @@ const UpdateNotification = {
neverShowAgain () {
this.toggleShow()
this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER })
- this.$store.commit('setFlag', { flag: 'dontShowUpdateNotifs', value: 1 })
+ this.$store.commit('setPreference', { path: 'simple.dontShowUpdateNotifs', value: true })
this.$store.dispatch('pushServerSideStorage')
},
dismiss () {
diff --git a/src/components/update_notification/update_notification.vue b/src/components/update_notification/update_notification.vue
index d0e2499c..00841af2 100644
--- a/src/components/update_notification/update_notification.vue
+++ b/src/components/update_notification/update_notification.vue
@@ -60,7 +60,7 @@
<template #linkToArtist>
<a
target="_blank"
- href="https://post.ebin.club/pipivovott"
+ href="https://post.ebin.club/users/pipivovott"
>pipivovott</a>
</template>
</i18n-t>
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 4c81e2c7..37458ba2 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -5,6 +5,7 @@ import FollowButton from '../follow_button/follow_button.vue'
import ModerationTools from '../moderation_tools/moderation_tools.vue'
import AccountActions from '../account_actions/account_actions.vue'
import Select from '../select/select.vue'
+import UserLink from '../user_link/user_link.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters } from 'vuex'
@@ -134,7 +135,8 @@ export default {
ProgressButton,
FollowButton,
Select,
- RichContent
+ RichContent,
+ UserLink
},
methods: {
muteUser () {
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index ace89c51..b84ff27d 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -106,13 +106,10 @@
</button>
</div>
<div class="bottom-line">
- <router-link
+ <user-link
class="user-screen-name"
- :title="user.screen_name_ui"
- :to="userProfileLink(user)"
- >
- @{{ user.screen_name_ui }}
- </router-link>
+ :user="user"
+ />
<template v-if="!hideBio">
<span
v-if="user.deactivated"
diff --git a/src/components/user_link/user_link.vue b/src/components/user_link/user_link.vue
new file mode 100644
index 00000000..efd96e12
--- /dev/null
+++ b/src/components/user_link/user_link.vue
@@ -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>
diff --git a/src/components/user_list_menu/user_list_menu.js b/src/components/user_list_menu/user_list_menu.js
new file mode 100644
index 00000000..21996031
--- /dev/null
+++ b/src/components/user_list_menu/user_list_menu.js
@@ -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
diff --git a/src/components/user_list_menu/user_list_menu.vue b/src/components/user_list_menu/user_list_menu.vue
new file mode 100644
index 00000000..06947ab7
--- /dev/null
+++ b/src/components/user_list_menu/user_list_menu.vue
@@ -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>
diff --git a/src/components/user_list_popover/user_list_popover.js b/src/components/user_list_popover/user_list_popover.js
index e24eb9f7..046e0abd 100644
--- a/src/components/user_list_popover/user_list_popover.js
+++ b/src/components/user_list_popover/user_list_popover.js
@@ -1,5 +1,6 @@
import { defineAsyncComponent } from 'vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
+import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
@@ -15,6 +16,7 @@ const UserListPopover = {
],
components: {
RichContent,
+ UnicodeDomainIndicator,
Popover: defineAsyncComponent(() => import('../popover/popover.vue')),
UserAvatar: defineAsyncComponent(() => import('../user_avatar/user_avatar.vue'))
},
diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue
index a3ce54c3..635dc7f6 100644
--- a/src/components/user_list_popover/user_list_popover.vue
+++ b/src/components/user_list_popover/user_list_popover.vue
@@ -29,7 +29,7 @@
:emoji="user.emoji"
/>
<!-- eslint-enable vue/no-v-html -->
- <span class="user-list-screen-name">{{ user.screen_name_ui }}</span>
+ <span class="user-list-screen-name">{{ user.screen_name_ui }}</span><UnicodeDomainIndicator :user="user" />
</div>
</div>
</template>
diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js
index 85ffc661..67fde084 100644
--- a/src/components/user_reporting_modal/user_reporting_modal.js
+++ b/src/components/user_reporting_modal/user_reporting_modal.js
@@ -2,13 +2,15 @@ import Status from '../status/status.vue'
import List from '../list/list.vue'
import Checkbox from '../checkbox/checkbox.vue'
import Modal from '../modal/modal.vue'
+import UserLink from '../user_link/user_link.vue'
const UserReportingModal = {
components: {
Status,
List,
Checkbox,
- Modal
+ Modal,
+ UserLink
},
data () {
return {
diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue
index 429a66e2..8c42ab7b 100644
--- a/src/components/user_reporting_modal/user_reporting_modal.vue
+++ b/src/components/user_reporting_modal/user_reporting_modal.vue
@@ -5,9 +5,13 @@
>
<div class="user-reporting-panel panel">
<div class="panel-heading">
- <div class="title">
- {{ $t('user_reporting.title', [user.screen_name_ui]) }}
- </div>
+ <i18n-t
+ tag="div"
+ keypath="user_reporting.title"
+ class="title"
+ >
+ <UserLink :user="user" />
+ </i18n-t>
</div>
<div class="panel-body">
<div class="user-reporting-panel-left">
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 91722d9a..ffa4c6b0 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -80,11 +80,16 @@
"confirm": "Confirm",
"verify": "Verify",
"close": "Close",
+ "undo": "Undo",
+ "yes": "Yes",
+ "no": "No",
"peek": "Peek",
"role": {
"admin": "Admin",
"moderator": "Moderator"
},
+ "unpin": "Unpin item",
+ "pin": "Pin item",
"flash_content": "Click to show Flash content using Ruffle (Experimental, may not work).",
"flash_security": "Note that this can be potentially dangerous since Flash content is still arbitrary code.",
"flash_fail": "Failed to load flash content, see console for details.",
@@ -149,7 +154,10 @@
"preferences": "Preferences",
"timelines": "Timelines",
"chats": "Chats",
- "lists": "Lists"
+ "lists": "Lists",
+ "edit_nav_mobile": "Customize navigation bar",
+ "edit_pinned": "Edit pinned items",
+ "edit_finish": "Done editing"
},
"notifications": {
"broken_favorite": "Unknown status, searching for it…",
@@ -987,7 +995,18 @@
"create": "Create",
"save": "Save changes",
"delete": "Delete list",
- "following_only": "Limit to Following"
+ "following_only": "Limit to Following",
+ "manage_lists": "Manage lists",
+ "manage_members": "Manage list members",
+ "add_members": "Search for more users",
+ "remove_from_list": "Remove from list",
+ "add_to_list": "Add to list",
+ "is_in_list": "Already in list",
+ "editing_list": "Editing list {listTitle}",
+ "creating_list": "Creating new list",
+ "update_title": "Save Title",
+ "really_delete": "Really delete list?",
+ "error": "Error manipulating lists: {0}"
},
"file_type": {
"audio": "Audio",
@@ -1006,5 +1025,8 @@
"update_changelog": "For more details on what's changed, see {theFullChangelog}.",
"update_changelog_here": "the full changelog",
"art_by": "Art by {linkToArtist}"
+ },
+ "unicode_domain_indicator": {
+ "tooltip": "This domain contains non-ascii characters."
}
}
diff --git a/src/modules/api.js b/src/modules/api.js
index 80a978f9..f783fa4f 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -15,6 +15,9 @@ const api = {
mastoUserSocketStatus: null,
followRequests: []
},
+ getters: {
+ followRequestCount: state => state.api.followRequests.length
+ },
mutations: {
setBackendInteractor (state, backendInteractor) {
state.backendInteractor = backendInteractor
diff --git a/src/modules/config.js b/src/modules/config.js
index be30dee3..eeaac917 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -89,7 +89,6 @@ export const defaultState = {
contentColumnWidth: '45rem',
notifsColumnWidth: '25rem',
navbarColumnStretch: false,
- listsNavigation: false,
greentext: undefined, // instance default
useAtIcon: undefined, // instance default
mentionLinkDisplay: undefined, // instance default
diff --git a/src/modules/lists.js b/src/modules/lists.js
index 84c15759..22fed800 100644
--- a/src/modules/lists.js
+++ b/src/modules/lists.js
@@ -9,27 +9,43 @@ export const mutations = {
setLists (state, value) {
state.allLists = value
},
- setList (state, { id, title }) {
- if (!state.allListsObject[id]) {
- state.allListsObject[id] = {}
+ setList (state, { listId, title }) {
+ if (!state.allListsObject[listId]) {
+ state.allListsObject[listId] = { accountIds: [] }
}
- state.allListsObject[id].title = title
+ state.allListsObject[listId].title = title
- if (!find(state.allLists, { id })) {
- state.allLists.push({ id, title })
+ const entry = find(state.allLists, { id: listId })
+ if (!entry) {
+ state.allLists.push({ id: listId, title })
} else {
- find(state.allLists, { id }).title = title
+ entry.title = title
}
},
- setListAccounts (state, { id, accountIds }) {
- if (!state.allListsObject[id]) {
- state.allListsObject[id] = {}
+ setListAccounts (state, { listId, accountIds }) {
+ if (!state.allListsObject[listId]) {
+ state.allListsObject[listId] = { accountIds: [] }
}
- state.allListsObject[id].accountIds = accountIds
+ state.allListsObject[listId].accountIds = accountIds
},
- deleteList (state, { id }) {
- delete state.allListsObject[id]
- remove(state.allLists, list => list.id === id)
+ addListAccount (state, { listId, accountId }) {
+ if (!state.allListsObject[listId]) {
+ state.allListsObject[listId] = { accountIds: [] }
+ }
+ state.allListsObject[listId].accountIds.push(accountId)
+ },
+ removeListAccount (state, { listId, accountId }) {
+ if (!state.allListsObject[listId]) {
+ state.allListsObject[listId] = { accountIds: [] }
+ }
+ const { accountIds } = state.allListsObject[listId]
+ const set = new Set(accountIds)
+ set.delete(accountId)
+ state.allListsObject[listId].accountIds = [...set]
+ },
+ deleteList (state, { listId }) {
+ delete state.allListsObject[listId]
+ remove(state.allLists, list => list.id === listId)
}
}
@@ -40,37 +56,57 @@ const actions = {
createList ({ rootState, commit }, { title }) {
return rootState.api.backendInteractor.createList({ title })
.then((list) => {
- commit('setList', { id: list.id, title })
+ commit('setList', { listId: list.id, title })
return list
})
},
- fetchList ({ rootState, commit }, { id }) {
- return rootState.api.backendInteractor.getList({ id })
- .then((list) => commit('setList', { id: list.id, title: list.title }))
+ fetchList ({ rootState, commit }, { listId }) {
+ return rootState.api.backendInteractor.getList({ listId })
+ .then((list) => commit('setList', { listId: list.id, title: list.title }))
},
- fetchListAccounts ({ rootState, commit }, { id }) {
- return rootState.api.backendInteractor.getListAccounts({ id })
- .then((accountIds) => commit('setListAccounts', { id, accountIds }))
+ fetchListAccounts ({ rootState, commit }, { listId }) {
+ return rootState.api.backendInteractor.getListAccounts({ listId })
+ .then((accountIds) => commit('setListAccounts', { listId, accountIds }))
},
- setList ({ rootState, commit }, { id, title }) {
- rootState.api.backendInteractor.updateList({ id, title })
- commit('setList', { id, title })
+ setList ({ rootState, commit }, { listId, title }) {
+ rootState.api.backendInteractor.updateList({ listId, title })
+ commit('setList', { listId, title })
},
- setListAccounts ({ rootState, commit }, { id, accountIds }) {
- const saved = rootState.lists.allListsObject[id].accountIds || []
+ setListAccounts ({ rootState, commit }, { listId, accountIds }) {
+ const saved = rootState.lists.allListsObject[listId].accountIds || []
const added = accountIds.filter(id => !saved.includes(id))
const removed = saved.filter(id => !accountIds.includes(id))
- commit('setListAccounts', { id, accountIds })
+ commit('setListAccounts', { listId, accountIds })
if (added.length > 0) {
- rootState.api.backendInteractor.addAccountsToList({ id, accountIds: added })
+ rootState.api.backendInteractor.addAccountsToList({ listId, accountIds: added })
}
if (removed.length > 0) {
- rootState.api.backendInteractor.removeAccountsFromList({ id, accountIds: removed })
+ rootState.api.backendInteractor.removeAccountsFromList({ listId, accountIds: removed })
}
},
- deleteList ({ rootState, commit }, { id }) {
- rootState.api.backendInteractor.deleteList({ id })
- commit('deleteList', { id })
+ addListAccount ({ rootState, commit }, { listId, accountId }) {
+ return rootState
+ .api
+ .backendInteractor
+ .addAccountsToList({ listId, accountIds: [accountId] })
+ .then((result) => {
+ commit('addListAccount', { listId, accountId })
+ return result
+ })
+ },
+ removeListAccount ({ rootState, commit }, { listId, accountId }) {
+ return rootState
+ .api
+ .backendInteractor
+ .removeAccountsFromList({ listId, accountIds: [accountId] })
+ .then((result) => {
+ commit('removeListAccount', { listId, accountId })
+ return result
+ })
+ },
+ deleteList ({ rootState, commit }, { listId }) {
+ rootState.api.backendInteractor.deleteList({ listId })
+ commit('deleteList', { listId })
}
}
diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js
index e516a6e6..56164be7 100644
--- a/src/modules/serverSideStorage.js
+++ b/src/modules/serverSideStorage.js
@@ -1,5 +1,5 @@
import { toRaw } from 'vue'
-import { isEqual, cloneDeep } from 'lodash'
+import { isEqual, cloneDeep, set, get, clamp, flatten, groupBy, findLastIndex, takeRight } from 'lodash'
import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js'
export const VERSION = 1
@@ -14,14 +14,21 @@ export const defaultState = {
// storage of flags - stuff that can only be set and incremented
flagStorage: {
updateCounter: 0, // Counter for most recent update notification seen
- // TODO move to prefsStorage when that becomes a thing since only way
- // this can be reset is by complete reset of all flags
- dontShowUpdateNotifs: 0, // if user chose to not show update notifications ever again
reset: 0 // special flag that can be used to force-reset all flags, debug purposes only
// special reset codes:
// 1000: trim keys to those known by currently running FE
// 1001: same as above + reset everything to 0
},
+ prefsStorage: {
+ _journal: [],
+ simple: {
+ dontShowUpdateNotifs: false,
+ collapseNav: false
+ },
+ collections: {
+ pinnedNavItems: ['home', 'dms', 'chats']
+ }
+ },
// raw data
raw: null,
// local cache
@@ -33,14 +40,43 @@ export const newUserFlags = {
updateCounter: CURRENT_UPDATE_COUNTER // new users don't need to see update notification
}
-const _wrapData = (data) => ({
+export const _moveItemInArray = (array, value, movement) => {
+ const oldIndex = array.indexOf(value)
+ const newIndex = oldIndex + movement
+ const newArray = [...array]
+ // remove old
+ newArray.splice(oldIndex, 1)
+ // add new
+ newArray.splice(clamp(newIndex, 0, newArray.length + 1), 0, value)
+ return newArray
+}
+
+const _wrapData = (data, userName) => ({
...data,
+ _user: userName,
_timestamp: Date.now(),
_version: VERSION
})
const _checkValidity = (data) => data._timestamp > 0 && data._version > 0
+const _verifyPrefs = (state) => {
+ state.prefsStorage = state.prefsStorage || {
+ simple: {},
+ collections: {}
+ }
+ Object.entries(defaultState.prefsStorage.simple).forEach(([k, v]) => {
+ if (typeof v === 'number' || typeof v === 'boolean') return
+ console.warn(`Preference simple.${k} as invalid type, reinitializing`)
+ set(state.prefsStorage.simple, k, defaultState.prefsStorage.simple[k])
+ })
+ Object.entries(defaultState.prefsStorage.collections).forEach(([k, v]) => {
+ if (Array.isArray(v)) return
+ console.warn(`Preference collections.${k} as invalid type, reinitializing`)
+ set(state.prefsStorage.collections, k, defaultState.prefsStorage.collections[k])
+ })
+}
+
export const _getRecentData = (cache, live) => {
const result = { recent: null, stale: null, needUpload: false }
const cacheValid = _checkValidity(cache || {})
@@ -85,6 +121,8 @@ export const _getAllFlags = (recent, stale) => {
}
export const _mergeFlags = (recent, stale, allFlagKeys) => {
+ if (!stale.flagStorage) return recent.flagStorage
+ if (!recent.flagStorage) return stale.flagStorage
return Object.fromEntries(allFlagKeys.map(flag => {
const recentFlag = recent.flagStorage[flag]
const staleFlag = stale.flagStorage[flag]
@@ -93,6 +131,88 @@ export const _mergeFlags = (recent, stale, allFlagKeys) => {
}))
}
+const _mergeJournal = (...journals) => {
+ // Ignore invalid journal entries
+ const allJournals = flatten(
+ journals.map(j => Array.isArray(j) ? j : [])
+ ).filter(entry =>
+ Object.prototype.hasOwnProperty.call(entry, 'path') &&
+ Object.prototype.hasOwnProperty.call(entry, 'operation') &&
+ Object.prototype.hasOwnProperty.call(entry, 'args') &&
+ Object.prototype.hasOwnProperty.call(entry, 'timestamp')
+ )
+ const grouped = groupBy(allJournals, 'path')
+ const trimmedGrouped = Object.entries(grouped).map(([path, journal]) => {
+ // side effect
+ journal.sort((a, b) => a.timestamp > b.timestamp ? 1 : -1)
+
+ if (path.startsWith('collections')) {
+ const lastRemoveIndex = findLastIndex(journal, ({ operation }) => operation === 'removeFromCollection')
+ // everything before last remove is unimportant
+ if (lastRemoveIndex > 0) {
+ return journal.slice(lastRemoveIndex)
+ } else {
+ // everything else doesn't need trimming
+ return journal
+ }
+ } else if (path.startsWith('simple')) {
+ // Only the last record is important
+ return takeRight(journal)
+ } else {
+ return journal
+ }
+ })
+ return flatten(trimmedGrouped)
+ .sort((a, b) => a.timestamp > b.timestamp ? 1 : -1)
+}
+
+export const _mergePrefs = (recent, stale, allFlagKeys) => {
+ if (!stale) return recent
+ if (!recent) return stale
+ const { _journal: recentJournal, ...recentData } = recent
+ const { _journal: staleJournal } = stale
+ /** Journal entry format:
+ * path: path to entry in prefsStorage
+ * timestamp: timestamp of the change
+ * operation: operation type
+ * arguments: array of arguments, depends on operation type
+ *
+ * currently only supported operation type is "set" which just sets the value
+ * to requested one. Intended only to be used with simple preferences (boolean, number)
+ * shouldn't be used with collections!
+ */
+ const resultOutput = { ...recentData }
+ const totalJournal = _mergeJournal(staleJournal, recentJournal)
+ totalJournal.forEach(({ path, timestamp, operation, command, args }) => {
+ if (path.startsWith('_')) {
+ console.error(`journal contains entry to edit internal (starts with _) field '${path}', something is incorrect here, ignoring.`)
+ return
+ }
+ switch (operation) {
+ case 'set':
+ set(resultOutput, path, args[0])
+ break
+ case 'addToCollection':
+ set(resultOutput, path, Array.from(new Set(get(resultOutput, path)).add(args[0])))
+ break
+ case 'removeFromCollection': {
+ const newSet = new Set(get(resultOutput, path))
+ newSet.delete(args[0])
+ set(resultOutput, path, Array.from(newSet))
+ break
+ }
+ case 'reorderCollection': {
+ const [value, movement] = args
+ set(resultOutput, path, _moveItemInArray(get(resultOutput, path), value, movement))
+ break
+ }
+ default:
+ console.error(`Unknown journal operation: '${operation}', did we forget to run reverse migrations beforehand?`)
+ }
+ })
+ return { ...resultOutput, _journal: totalJournal }
+}
+
export const _resetFlags = (totalFlags, knownKeys = defaultState.flagStorage) => {
let result = { ...totalFlags }
const allFlagKeys = Object.keys(totalFlags)
@@ -149,10 +269,17 @@ export const _doMigrations = (cache) => {
}
export const mutations = {
+ clearServerSideStorage (state, userData) {
+ state = { ...cloneDeep(defaultState) }
+ },
setServerSideStorage (state, userData) {
const live = userData.storage
state.raw = live
let cache = state.cache
+ if (cache && cache._user !== userData.fqn) {
+ console.warn('cache belongs to another user! reinitializing local cache!')
+ cache = null
+ }
cache = _doMigrations(cache)
@@ -165,7 +292,8 @@ export const mutations = {
if (recent === null) {
console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`)
recent = _wrapData({
- flagStorage: { ...flagsTemplate }
+ flagStorage: { ...flagsTemplate },
+ prefsStorage: { ...defaultState.prefsStorage }
})
}
@@ -180,17 +308,23 @@ export const mutations = {
const allFlagKeys = _getAllFlags(recent, stale)
let totalFlags
+ let totalPrefs
if (dirty) {
// Merge the flags
- console.debug('Merging the flags...')
+ console.debug('Merging the data...')
totalFlags = _mergeFlags(recent, stale, allFlagKeys)
+ _verifyPrefs(recent)
+ _verifyPrefs(stale)
+ totalPrefs = _mergePrefs(recent.prefsStorage, stale.prefsStorage)
} else {
totalFlags = recent.flagStorage
+ totalPrefs = recent.prefsStorage
}
totalFlags = _resetFlags(totalFlags)
- recent.flagStorage = totalFlags
+ recent.flagStorage = { ...flagsTemplate, ...totalFlags }
+ recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs }
state.dirty = dirty || needsUpload
state.cache = recent
@@ -199,10 +333,72 @@ export const mutations = {
state.cache._timestamp = Math.min(stale._timestamp, recent._timestamp)
}
state.flagStorage = state.cache.flagStorage
+ state.prefsStorage = state.cache.prefsStorage
},
setFlag (state, { flag, value }) {
state.flagStorage[flag] = value
state.dirty = true
+ },
+ setPreference (state, { path, value }) {
+ if (path.startsWith('_')) {
+ console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
+ return
+ }
+ set(state.prefsStorage, path, value)
+ state.prefsStorage._journal = [
+ ...state.prefsStorage._journal,
+ { operation: 'set', path, args: [value], timestamp: Date.now() }
+ ]
+ state.dirty = true
+ },
+ addCollectionPreference (state, { path, value }) {
+ if (path.startsWith('_')) {
+ console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
+ return
+ }
+ const collection = new Set(get(state.prefsStorage, path))
+ collection.add(value)
+ set(state.prefsStorage, path, [...collection])
+ state.prefsStorage._journal = [
+ ...state.prefsStorage._journal,
+ { operation: 'addToCollection', path, args: [value], timestamp: Date.now() }
+ ]
+ state.dirty = true
+ },
+ removeCollectionPreference (state, { path, value }) {
+ if (path.startsWith('_')) {
+ console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
+ return
+ }
+ const collection = new Set(get(state.prefsStorage, path))
+ collection.delete(value)
+ set(state.prefsStorage, path, [...collection])
+ state.prefsStorage._journal = [
+ ...state.prefsStorage._journal,
+ { operation: 'removeFromCollection', path, args: [value], timestamp: Date.now() }
+ ]
+ state.dirty = true
+ },
+ reorderCollectionPreference (state, { path, value, movement }) {
+ if (path.startsWith('_')) {
+ console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
+ return
+ }
+ const collection = get(state.prefsStorage, path)
+ const newCollection = _moveItemInArray(collection, value, movement)
+ set(state.prefsStorage, path, newCollection)
+ state.prefsStorage._journal = [
+ ...state.prefsStorage._journal,
+ { operation: 'arrangeCollection', path, args: [value], timestamp: Date.now() }
+ ]
+ state.dirty = true
+ },
+ updateCache (state, { username }) {
+ state.prefsStorage._journal = _mergeJournal(state.prefsStorage._journal)
+ state.cache = _wrapData({
+ flagStorage: toRaw(state.flagStorage),
+ prefsStorage: toRaw(state.prefsStorage)
+ }, username)
}
}
@@ -214,15 +410,16 @@ const serverSideStorage = {
actions: {
pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) {
const needPush = state.dirty || force
+ console.log(needPush)
if (!needPush) return
- state.cache = _wrapData({
- flagStorage: toRaw(state.flagStorage)
- })
+ commit('updateCache', { username: rootState.users.currentUser.fqn })
const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } }
rootState.api.backendInteractor
.updateProfile({ params })
- .then((user) => commit('setServerSideStorage', user))
- state.dirty = false
+ .then((user) => {
+ commit('setServerSideStorage', user)
+ state.dirty = false
+ })
}
}
}
diff --git a/src/modules/users.js b/src/modules/users.js
index be0bc997..de28766a 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -171,6 +171,9 @@ export const mutations = {
state.relationships[relationship.id] = relationship
})
},
+ updateUserInLists (state, { id, inLists }) {
+ state.usersObject[id].inLists = inLists
+ },
saveBlockIds (state, blockIds) {
state.currentUser.blockIds = blockIds
},
@@ -298,6 +301,12 @@ const users = {
.then((relationships) => store.commit('updateUserRelationship', relationships))
}
},
+ fetchUserInLists (store, id) {
+ if (store.state.currentUser) {
+ store.rootState.api.backendInteractor.fetchUserInLists({ id })
+ .then((inLists) => store.commit('updateUserInLists', { id, inLists }))
+ }
+ },
fetchBlocks (store) {
return store.rootState.api.backendInteractor.fetchBlocks()
.then((blocks) => {
@@ -509,6 +518,7 @@ const users = {
store.dispatch('stopFetchingTimeline', 'friends')
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
store.dispatch('stopFetchingNotifications')
+ store.dispatch('stopFetchingLists')
store.dispatch('stopFetchingFollowRequests')
store.commit('clearNotifications')
store.commit('resetStatuses')
@@ -516,6 +526,7 @@ const users = {
store.dispatch('setLastTimeline', 'public-timeline')
store.dispatch('setLayoutWidth', windowWidth())
store.dispatch('setLayoutHeight', windowHeight())
+ store.commit('clearServerSideStorage')
})
},
loginUser (store, accessToken) {
@@ -562,6 +573,12 @@ const users = {
store.dispatch('startFetchingChats')
}
+ store.dispatch('startFetchingLists')
+
+ if (user.locked) {
+ store.dispatch('startFetchingFollowRequests')
+ }
+
if (store.getters.mergedConfig.useStreamingApi) {
store.dispatch('fetchTimeline', 'friends', { since: null })
store.dispatch('fetchNotifications', { since: null })
diff --git a/src/panel.scss b/src/panel.scss
index 3a814269..2e769e27 100644
--- a/src/panel.scss
+++ b/src/panel.scss
@@ -46,7 +46,7 @@
.panel-footer {
--panel-heading-height-padding: 0.6em;
--__panel-heading-height: 3.2em;
- --__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding));
+ --__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding, 0));
position: relative;
box-sizing: border-box;
@@ -57,7 +57,7 @@
grid-column-gap: 0.5em;
flex: none;
background-size: cover;
- padding: 0.6em;
+ padding: var(--panel-heading-height-padding);
height: var(--__panel-heading-height);
line-height: var(--__panel-heading-height-inner);
z-index: 4;
@@ -147,6 +147,15 @@
color: var(--panelLink, $fallback--link);
}
+ .button-unstyled:hover,
+ a:hover {
+ i[class*=icon-],
+ .svg-inline--fa,
+ .iconLetter {
+ color: var(--panelText);
+ }
+ }
+
.faint {
background-color: transparent;
color: $fallback--faint;
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index ce60d65a..dd85b281 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -53,6 +53,7 @@ const MASTODON_USER_URL = '/api/v1/accounts'
const MASTODON_USER_LOOKUP_URL = '/api/v1/accounts/lookup'
const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
+const MASTODON_USER_IN_LISTS = id => `/api/v1/accounts/${id}/lists`
const MASTODON_LIST_URL = id => `/api/v1/lists/${id}`
const MASTODON_LIST_TIMELINE_URL = id => `/api/v1/timelines/list/${id}`
const MASTODON_LIST_ACCOUNTS_URL = id => `/api/v1/lists/${id}/accounts`
@@ -263,6 +264,13 @@ const unfollowUser = ({ id, credentials }) => {
}).then((data) => data.json())
}
+const fetchUserInLists = ({ id, credentials }) => {
+ const url = MASTODON_USER_IN_LISTS(id)
+ return fetch(url, {
+ headers: authHeaders(credentials)
+ }).then((data) => data.json())
+}
+
const pinOwnStatus = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_PIN_OWN_STATUS(id), credentials, method: 'POST' })
.then((data) => parseStatus(data))
@@ -428,14 +436,14 @@ const createList = ({ title, credentials }) => {
}).then((data) => data.json())
}
-const getList = ({ id, credentials }) => {
- const url = MASTODON_LIST_URL(id)
+const getList = ({ listId, credentials }) => {
+ const url = MASTODON_LIST_URL(listId)
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
}
-const updateList = ({ id, title, credentials }) => {
- const url = MASTODON_LIST_URL(id)
+const updateList = ({ listId, title, credentials }) => {
+ const url = MASTODON_LIST_URL(listId)
const headers = authHeaders(credentials)
headers['Content-Type'] = 'application/json'
@@ -446,15 +454,15 @@ const updateList = ({ id, title, credentials }) => {
})
}
-const getListAccounts = ({ id, credentials }) => {
- const url = MASTODON_LIST_ACCOUNTS_URL(id)
+const getListAccounts = ({ listId, credentials }) => {
+ const url = MASTODON_LIST_ACCOUNTS_URL(listId)
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
.then((data) => data.map(({ id }) => id))
}
-const addAccountsToList = ({ id, accountIds, credentials }) => {
- const url = MASTODON_LIST_ACCOUNTS_URL(id)
+const addAccountsToList = ({ listId, accountIds, credentials }) => {
+ const url = MASTODON_LIST_ACCOUNTS_URL(listId)
const headers = authHeaders(credentials)
headers['Content-Type'] = 'application/json'
@@ -465,8 +473,8 @@ const addAccountsToList = ({ id, accountIds, credentials }) => {
})
}
-const removeAccountsFromList = ({ id, accountIds, credentials }) => {
- const url = MASTODON_LIST_ACCOUNTS_URL(id)
+const removeAccountsFromList = ({ listId, accountIds, credentials }) => {
+ const url = MASTODON_LIST_ACCOUNTS_URL(listId)
const headers = authHeaders(credentials)
headers['Content-Type'] = 'application/json'
@@ -477,8 +485,8 @@ const removeAccountsFromList = ({ id, accountIds, credentials }) => {
})
}
-const deleteList = ({ id, credentials }) => {
- const url = MASTODON_LIST_URL(id)
+const deleteList = ({ listId, credentials }) => {
+ const url = MASTODON_LIST_URL(listId)
return fetch(url, {
method: 'DELETE',
headers: authHeaders(credentials)
@@ -1584,7 +1592,8 @@ const apiService = {
sendChatMessage,
readChat,
deleteChatMessage,
- setReportState
+ setReportState,
+ fetchUserInLists
}
export default apiService
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index e9cbcfe6..451427da 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -43,11 +43,13 @@ export const parseUser = (data) => {
// case for users in "mentions" property for statuses in MastoAPI
const mastoShort = masto && !Object.prototype.hasOwnProperty.call(data, 'avatar')
+ output.inLists = null
output.id = String(data.id)
output._original = data // used for server-side settings
if (masto) {
output.screen_name = data.acct
+ output.fqn = data.fqn
output.statusnet_profile_url = data.url
// There's nothing else to get
@@ -214,12 +216,14 @@ export const parseUser = (data) => {
output.screen_name_ui = output.screen_name
if (output.screen_name && output.screen_name.includes('@')) {
const parts = output.screen_name.split('@')
- let unicodeDomain = punycode.toUnicode(parts[1])
+ const unicodeDomain = punycode.toUnicode(parts[1])
if (unicodeDomain !== parts[1]) {
// Add some identifier so users can potentially spot spoofing attempts:
// lain.com and xn--lin-6cd.com would appear identical otherwise.
- unicodeDomain = '🌏' + unicodeDomain
+ output.screen_name_ui_contains_non_ascii = true
output.screen_name_ui = [parts[0], unicodeDomain].join('@')
+ } else {
+ output.screen_name_ui_contains_non_ascii = false
}
}