From a4e3cccf1cba238e5bfd96ea8c60f0d12bc6b7aa Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson Date: Wed, 6 Jan 2021 18:31:34 +0200 Subject: somewhat workign version still with fixture --- src/modules/config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/modules/config.js') diff --git a/src/modules/config.js b/src/modules/config.js index cd088737..e591a506 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -44,8 +44,9 @@ export const defaultState = { likes: true, repeats: true, moves: true, - emojiReactions: false, + emojiReactions: true, followRequest: true, + reports: true, chatMention: true }, webPushNotifications: false, -- cgit v1.2.3-70-g09d2 From 171f6f08943dd1d87120df3e4894ddcfd5e1d246 Mon Sep 17 00:00:00 2001 From: Alexander Tumin Date: Sat, 6 Aug 2022 17:26:43 +0300 Subject: Lists implementation --- src/boot/routes.js | 8 +- src/components/lists/lists.js | 32 ++++++ src/components/lists/lists.vue | 31 ++++++ src/components/lists_card/lists_card.js | 16 +++ src/components/lists_card/lists_card.vue | 51 ++++++++++ src/components/lists_edit/lists_edit.js | 91 +++++++++++++++++ src/components/lists_edit/lists_edit.vue | 108 +++++++++++++++++++++ src/components/lists_menu/lists_menu_content.js | 33 +++++++ src/components/lists_menu/lists_menu_content.vue | 17 ++++ src/components/lists_new/lists_new.js | 79 +++++++++++++++ src/components/lists_new/lists_new.vue | 95 ++++++++++++++++++ src/components/lists_timeline/lists_timeline.js | 36 +++++++ src/components/lists_timeline/lists_timeline.vue | 10 ++ .../lists_user_search/lists_user_search.js | 46 +++++++++ .../lists_user_search/lists_user_search.vue | 45 +++++++++ src/components/nav_panel/nav_panel.js | 19 +++- src/components/nav_panel/nav_panel.vue | 45 +++++++++ src/components/settings_modal/tabs/general_tab.vue | 47 +++++++++ src/components/side_drawer/side_drawer.js | 6 +- src/components/side_drawer/side_drawer.vue | 12 +++ src/components/timeline/timeline.js | 3 + src/components/timeline_menu/timeline_menu.js | 3 + src/i18n/en.json | 14 ++- src/main.js | 2 + src/modules/api.js | 17 +++- src/modules/config.js | 4 + src/modules/lists.js | 94 ++++++++++++++++++ src/modules/statuses.js | 3 +- src/services/api/api.service.js | 93 ++++++++++++++++++ .../backend_interactor_service.js | 9 +- .../lists_fetcher/lists_fetcher.service.js | 22 +++++ .../timeline_fetcher/timeline_fetcher.service.js | 14 ++- test/unit/specs/boot/routes.spec.js | 24 +++++ test/unit/specs/modules/lists.spec.js | 83 ++++++++++++++++ 34 files changed, 1194 insertions(+), 18 deletions(-) create mode 100644 src/components/lists/lists.js create mode 100644 src/components/lists/lists.vue create mode 100644 src/components/lists_card/lists_card.js create mode 100644 src/components/lists_card/lists_card.vue create mode 100644 src/components/lists_edit/lists_edit.js create mode 100644 src/components/lists_edit/lists_edit.vue create mode 100644 src/components/lists_menu/lists_menu_content.js create mode 100644 src/components/lists_menu/lists_menu_content.vue create mode 100644 src/components/lists_new/lists_new.js create mode 100644 src/components/lists_new/lists_new.vue create mode 100644 src/components/lists_timeline/lists_timeline.js create mode 100644 src/components/lists_timeline/lists_timeline.vue create mode 100644 src/components/lists_user_search/lists_user_search.js create mode 100644 src/components/lists_user_search/lists_user_search.vue create mode 100644 src/modules/lists.js create mode 100644 src/services/lists_fetcher/lists_fetcher.service.js create mode 100644 test/unit/specs/modules/lists.spec.js (limited to 'src/modules/config.js') diff --git a/src/boot/routes.js b/src/boot/routes.js index c8194d5f..8bb7d196 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -20,6 +20,9 @@ import ShoutPanel from 'components/shout_panel/shout_panel.vue' import WhoToFollow from 'components/who_to_follow/who_to_follow.vue' import About from 'components/about/about.vue' import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue' +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' export default (store) => { const validateAuthenticatedRoute = (to, from, next) => { @@ -72,7 +75,10 @@ export default (store) => { { name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) }, { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute }, { name: 'about', path: '/about', component: About }, - { name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile } + { name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile }, + { name: 'lists', path: '/lists', component: Lists }, + { name: 'list-timeline', path: '/lists/:id', component: ListsTimeline }, + { name: 'list-edit', path: '/lists/:id/edit', component: ListsEdit } ] if (store.state.instance.pleromaChatMessagesAvailable) { diff --git a/src/components/lists/lists.js b/src/components/lists/lists.js new file mode 100644 index 00000000..791b99b2 --- /dev/null +++ b/src/components/lists/lists.js @@ -0,0 +1,32 @@ +import ListsCard from '../lists_card/lists_card.vue' +import ListsNew from '../lists_new/lists_new.vue' + +const Lists = { + data () { + return { + isNew: false + } + }, + components: { + ListsCard, + ListsNew + }, + created () { + this.$store.dispatch('startFetchingLists') + }, + computed: { + lists () { + return this.$store.state.lists.allLists + } + }, + methods: { + cancelNewList () { + this.isNew = false + }, + newList () { + this.isNew = true + } + } +} + +export default Lists diff --git a/src/components/lists/lists.vue b/src/components/lists/lists.vue new file mode 100644 index 00000000..fcc56447 --- /dev/null +++ b/src/components/lists/lists.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/components/lists_card/lists_card.js b/src/components/lists_card/lists_card.js new file mode 100644 index 00000000..b503caec --- /dev/null +++ b/src/components/lists_card/lists_card.js @@ -0,0 +1,16 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEllipsisH +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faEllipsisH +) + +const ListsCard = { + props: [ + 'list' + ] +} + +export default ListsCard diff --git a/src/components/lists_card/lists_card.vue b/src/components/lists_card/lists_card.vue new file mode 100644 index 00000000..c0d58f18 --- /dev/null +++ b/src/components/lists_card/lists_card.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/src/components/lists_edit/lists_edit.js b/src/components/lists_edit/lists_edit.js new file mode 100644 index 00000000..7b642929 --- /dev/null +++ b/src/components/lists_edit/lists_edit.js @@ -0,0 +1,91 @@ +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 UserAvatar from '../user_avatar/user_avatar.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: [] + } + }, + created () { + this.$store.dispatch('fetchList', { id: this.id }) + .then(() => { this.title = this.findListTitle(this.id) }) + this.$store.dispatch('fetchListAccounts', { id: this.id }) + .then(() => { + this.selectedUserIds = this.findListAccounts(this.id) + this.selectedUserIds.forEach(userId => { + this.$store.dispatch('fetchUserIfMissing', userId) + }) + }) + }, + computed: { + id () { + return this.$route.params.id + }, + users () { + return this.userIds.map(userId => this.findUser(userId)) + }, + selectedUsers () { + return this.selectedUserIds.map(userId => this.findUser(userId)).filter(user => user) + }, + ...mapState({ + currentUser: state => state.users.currentUser + }), + ...mapGetters(['findUser', 'findListTitle', 'findListAccounts']) + }, + methods: { + 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 + }, + 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: 'list-timeline', params: { id: this.id } }) + }, + deleteList () { + this.$store.dispatch('deleteList', { id: this.id }) + this.$router.push({ name: 'lists' }) + } + } +} + +export default ListsNew diff --git a/src/components/lists_edit/lists_edit.vue b/src/components/lists_edit/lists_edit.vue new file mode 100644 index 00000000..69007b02 --- /dev/null +++ b/src/components/lists_edit/lists_edit.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/src/components/lists_menu/lists_menu_content.js b/src/components/lists_menu/lists_menu_content.js new file mode 100644 index 00000000..37e7868c --- /dev/null +++ b/src/components/lists_menu/lists_menu_content.js @@ -0,0 +1,33 @@ +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 ListsMenuContent = { + created () { + this.$store.dispatch('startFetchingLists') + }, + computed: { + ...mapState({ + lists: state => state.lists.allLists, + currentUser: state => state.users.currentUser, + privateMode: state => state.instance.private, + federating: state => state.instance.federating + }) + } +} + +export default ListsMenuContent diff --git a/src/components/lists_menu/lists_menu_content.vue b/src/components/lists_menu/lists_menu_content.vue new file mode 100644 index 00000000..5606b74f --- /dev/null +++ b/src/components/lists_menu/lists_menu_content.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/lists_new/lists_new.js b/src/components/lists_new/lists_new.js new file mode 100644 index 00000000..7e3470fa --- /dev/null +++ b/src/components/lists_new/lists_new.js @@ -0,0 +1,79 @@ +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: 'list-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 new file mode 100644 index 00000000..4733bdde --- /dev/null +++ b/src/components/lists_new/lists_new.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/components/lists_timeline/lists_timeline.js b/src/components/lists_timeline/lists_timeline.js new file mode 100644 index 00000000..4611a736 --- /dev/null +++ b/src/components/lists_timeline/lists_timeline.js @@ -0,0 +1,36 @@ +import Timeline from '../timeline/timeline.vue' +const ListsTimeline = { + data () { + return { + listId: null + } + }, + components: { + Timeline + }, + computed: { + timeline () { return this.$store.state.statuses.timelines.list } + }, + watch: { + $route: function (route) { + if (route.name === 'list-timeline' && route.params.id !== this.listId) { + 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('startFetchingTimeline', { timeline: 'list', listId: this.listId }) + } + } + }, + created () { + this.listId = this.$route.params.id + this.$store.dispatch('fetchList', { id: this.listId }) + this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId }) + }, + unmounted () { + this.$store.dispatch('stopFetchingTimeline', 'list') + this.$store.commit('clearTimeline', { timeline: 'list' }) + } +} + +export default ListsTimeline diff --git a/src/components/lists_timeline/lists_timeline.vue b/src/components/lists_timeline/lists_timeline.vue new file mode 100644 index 00000000..18156b81 --- /dev/null +++ b/src/components/lists_timeline/lists_timeline.vue @@ -0,0 +1,10 @@ + + + diff --git a/src/components/lists_user_search/lists_user_search.js b/src/components/lists_user_search/lists_user_search.js new file mode 100644 index 00000000..5798841a --- /dev/null +++ b/src/components/lists_user_search/lists_user_search.js @@ -0,0 +1,46 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faSearch, + faChevronLeft +} from '@fortawesome/free-solid-svg-icons' +import { debounce } from 'lodash' +import Checkbox from '../checkbox/checkbox.vue' + +library.add( + faSearch, + faChevronLeft +) + +const ListsUserSearch = { + components: { + Checkbox + }, + data () { + return { + loading: false, + query: '', + followingOnly: true + } + }, + methods: { + onInput: debounce(function () { + this.search(this.query) + }, 2000), + search (query) { + if (!query) { + this.loading = false + return + } + + this.loading = true + 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)) + }) + } + } +} + +export default ListsUserSearch diff --git a/src/components/lists_user_search/lists_user_search.vue b/src/components/lists_user_search/lists_user_search.vue new file mode 100644 index 00000000..03fb3ba6 --- /dev/null +++ b/src/components/lists_user_search/lists_user_search.vue @@ -0,0 +1,45 @@ + + + + diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index 37bcb409..abeff6bf 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -1,4 +1,5 @@ import TimelineMenuContent from '../timeline_menu/timeline_menu_content.vue' +import ListsMenuContent from '../lists_menu/lists_menu_content.vue' import { mapState, mapGetters } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' @@ -12,7 +13,8 @@ import { faComments, faBell, faInfoCircle, - faStream + faStream, + faList } from '@fortawesome/free-solid-svg-icons' library.add( @@ -25,7 +27,8 @@ library.add( faComments, faBell, faInfoCircle, - faStream + faStream, + faList ) const NavPanel = { @@ -35,19 +38,27 @@ const NavPanel = { } }, components: { - TimelineMenuContent + TimelineMenuContent, + ListsMenuContent }, data () { return { - showTimelines: false + showTimelines: false, + showLists: false } }, methods: { toggleTimelines () { this.showTimelines = !this.showTimelines + }, + toggleLists () { + this.showLists = !this.showLists } }, computed: { + listsNavigation () { + return this.$store.getters.mergedConfig.listsNavigation + }, ...mapState({ currentUser: state => state.users.currentUser, followRequestCount: state => state.api.followRequests.length, diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 3fd27d89..9322e233 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -25,6 +25,51 @@ +
  • + +
    + +
    +
  • +
  • + + + +
  • +
  • + + {{ $t('settings.lists_navigation') }} + +
  • +
  • +

    {{ $t('settings.columns') }}

    +
  • +
  • + + {{ $t('settings.disable_sticky_headers') }} + +
  • +
  • + + {{ $t('settings.show_scrollbars') }} + +
  • +
  • + + {{ $t('settings.right_sidebar') }} + +
  • +
  • + + {{ $t('settings.third_column_mode') }} + +
  • +
  • + {{ $t('settings.column_sizes') }} +
    + + {{ $t('settings.column_sizes_' + column) }} + +
    +
  • diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index f45f8def..913fa695 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -14,7 +14,8 @@ import { faSearch, faTachometerAlt, faCog, - faInfoCircle + faInfoCircle, + faList } from '@fortawesome/free-solid-svg-icons' library.add( @@ -28,7 +29,8 @@ library.add( faSearch, faTachometerAlt, faCog, - faInfoCircle + faInfoCircle, + faList ) const SideDrawer = { diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 7547fb08..7d9d36d7 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -55,6 +55,18 @@ /> {{ $t("nav.timelines") }} +
  • + + {{ $t("nav.lists") }} + +
  • { if (statuses && statuses.length === 0) { diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js index a11e7b7e..0792dc13 100644 --- a/src/components/timeline_menu/timeline_menu.js +++ b/src/components/timeline_menu/timeline_menu.js @@ -58,6 +58,9 @@ const TimelineMenu = { if (route === 'tag-timeline') { return '#' + this.$route.params.tag } + if (route === 'list-timeline') { + return this.$store.getters.findListTitle(this.$route.params.id) + } const i18nkey = timelineNames()[this.$route.name] return i18nkey ? this.$t(i18nkey) : route } diff --git a/src/i18n/en.json b/src/i18n/en.json index a10b271a..2f9c372e 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -146,7 +146,8 @@ "who_to_follow": "Who to follow", "preferences": "Preferences", "timelines": "Timelines", - "chats": "Chats" + "chats": "Chats", + "lists": "Lists" }, "notifications": { "broken_favorite": "Unknown status, searching for it…", @@ -298,6 +299,7 @@ "desc": "To enable two-factor authentication, enter the code from your two-factor app:" } }, + "lists_navigation": "Show lists in navigation", "allow_following_move": "Allow auto-follow when following account moves", "attachmentRadius": "Attachments", "attachments": "Attachments", @@ -948,6 +950,16 @@ "error_sending_message": "Something went wrong when sending the message.", "empty_chat_list_placeholder": "You don't have any chats yet. Start a new chat!" }, + "lists": { + "lists": "Lists", + "new": "New List", + "title": "List title", + "search": "Search users", + "create": "Create", + "save": "Save changes", + "delete": "Delete list", + "following_only": "Limit to Following" + }, "file_type": { "audio": "Audio", "video": "Video", diff --git a/src/main.js b/src/main.js index eacd554c..7d2c82cb 100644 --- a/src/main.js +++ b/src/main.js @@ -6,6 +6,7 @@ import './lib/event_target_polyfill.js' import interfaceModule from './modules/interface.js' import instanceModule from './modules/instance.js' import statusesModule from './modules/statuses.js' +import listsModule from './modules/lists.js' import usersModule from './modules/users.js' import apiModule from './modules/api.js' import configModule from './modules/config.js' @@ -70,6 +71,7 @@ const persistedStateOptions = { // TODO refactor users/statuses modules, they depend on each other users: usersModule, statuses: statusesModule, + lists: listsModule, api: apiModule, config: configModule, serverSideConfig: serverSideConfigModule, diff --git a/src/modules/api.js b/src/modules/api.js index 28f2076e..80a978f9 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -191,12 +191,13 @@ const api = { startFetchingTimeline (store, { timeline = 'friends', tag = false, - userId = false + userId = false, + listId = false }) { if (store.state.fetchers[timeline]) return const fetcher = store.state.backendInteractor.startFetchingTimeline({ - timeline, store, userId, tag + timeline, store, userId, listId, tag }) store.commit('addFetcher', { fetcherName: timeline, fetcher }) }, @@ -248,6 +249,18 @@ const api = { store.commit('setFollowRequests', requests) }, + // Lists + startFetchingLists (store) { + if (store.state.fetchers.lists) return + const fetcher = store.state.backendInteractor.startFetchingLists({ store }) + store.commit('addFetcher', { fetcherName: 'lists', fetcher }) + }, + stopFetchingLists (store) { + const fetcher = store.state.fetchers.lists + if (!fetcher) return + store.commit('removeFetcher', { fetcherName: 'lists', fetcher }) + }, + // Pleroma websocket setWsToken (store, token) { store.commit('setWsToken', token) diff --git a/src/modules/config.js b/src/modules/config.js index eaf67a91..910fb172 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -83,6 +83,10 @@ export const defaultState = { showScrollbars: false, userPopoverZoom: false, userPopoverOverlay: true, + sidebarColumnWidth: '25rem', + contentColumnWidth: '45rem', + notifsColumnWidth: '25rem', + 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 new file mode 100644 index 00000000..58700f41 --- /dev/null +++ b/src/modules/lists.js @@ -0,0 +1,94 @@ +import { remove, find } from 'lodash' + +export const defaultState = { + allLists: [], + allListsObject: {} +} + +export const mutations = { + setLists (state, value) { + state.allLists = value + }, + setList (state, { id, title }) { + if (!state.allListsObject[id]) { + state.allListsObject[id] = {} + } + state.allListsObject[id].title = title + + if (!find(state.allLists, { id })) { + state.allLists.push({ id, title }) + } else { + find(state.allLists, { id }).title = title + } + }, + setListAccounts (state, { id, accountIds }) { + if (!state.allListsObject[id]) { + state.allListsObject[id] = {} + } + state.allListsObject[id].accountIds = accountIds + }, + deleteList (state, { id }) { + delete state.allListsObject[id] + remove(state.allLists, list => list.id === id) + } +} + +const actions = { + setLists ({ commit }, value) { + commit('setLists', value) + }, + createList ({ rootState, commit }, { title }) { + return rootState.api.backendInteractor.createList({ title }) + .then((list) => { + commit('setList', { id: 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 })) + }, + fetchListAccounts ({ rootState, commit }, { id }) { + return rootState.api.backendInteractor.getListAccounts({ id }) + .then((accountIds) => commit('setListAccounts', { id, accountIds })) + }, + setList ({ rootState, commit }, { id, title }) { + rootState.api.backendInteractor.updateList({ id, title }) + commit('setList', { id, title }) + }, + setListAccounts ({ rootState, commit }, { id, accountIds }) { + const saved = rootState.lists.allListsObject[id].accountIds + const added = accountIds.filter(id => !saved.includes(id)) + const removed = saved.filter(id => !accountIds.includes(id)) + commit('setListAccounts', { id, accountIds }) + if (added.length > 0) { + rootState.api.backendInteractor.addAccountsToList({ id, accountIds: added }) + } + if (removed.length > 0) { + rootState.api.backendInteractor.removeAccountsFromList({ id, accountIds: removed }) + } + }, + deleteList ({ rootState, commit }, { id }) { + rootState.api.backendInteractor.deleteList({ id }) + commit('deleteList', { id }) + } +} + +export const getters = { + findListTitle: state => id => { + if (!state.allListsObject[id]) return + return state.allListsObject[id].title + }, + findListAccounts: state => id => { + return [...state.allListsObject[id].accountIds] + } +} + +const lists = { + state: defaultState, + mutations, + actions, + getters +} + +export default lists diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 62251b0b..c4475005 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -62,7 +62,8 @@ export const defaultState = () => ({ friends: emptyTl(), tag: emptyTl(), dms: emptyTl(), - bookmarks: emptyTl() + bookmarks: emptyTl(), + list: emptyTl() } }) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 7b7fbefd..86d9eaf4 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -52,6 +52,9 @@ const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context` const MASTODON_USER_URL = '/api/v1/accounts' const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships' const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses` +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` const MASTODON_TAG_TIMELINE_URL = tag => `/api/v1/timelines/tag/${tag}` const MASTODON_BOOKMARK_TIMELINE_URL = '/api/v1/bookmarks' const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/' @@ -79,6 +82,7 @@ const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute` const MASTODON_SEARCH_2 = '/api/v2/search' const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search' const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks' +const MASTODON_LISTS_URL = '/api/v1/lists' const MASTODON_STREAMING = '/api/v1/streaming' const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers' const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions` @@ -385,6 +389,81 @@ const fetchFollowRequests = ({ credentials }) => { .then((data) => data.map(parseUser)) } +const fetchLists = ({ credentials }) => { + const url = MASTODON_LISTS_URL + return fetch(url, { headers: authHeaders(credentials) }) + .then((data) => data.json()) +} + +const createList = ({ title, credentials }) => { + const url = MASTODON_LISTS_URL + const headers = authHeaders(credentials) + headers['Content-Type'] = 'application/json' + + return fetch(url, { + headers, + method: 'POST', + body: JSON.stringify({ title }) + }).then((data) => data.json()) +} + +const getList = ({ id, credentials }) => { + const url = MASTODON_LIST_URL(id) + return fetch(url, { headers: authHeaders(credentials) }) + .then((data) => data.json()) +} + +const updateList = ({ id, title, credentials }) => { + const url = MASTODON_LIST_URL(id) + const headers = authHeaders(credentials) + headers['Content-Type'] = 'application/json' + + return fetch(url, { + headers, + method: 'PUT', + body: JSON.stringify({ title }) + }) +} + +const getListAccounts = ({ id, credentials }) => { + const url = MASTODON_LIST_ACCOUNTS_URL(id) + 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 headers = authHeaders(credentials) + headers['Content-Type'] = 'application/json' + + return fetch(url, { + headers, + method: 'POST', + body: JSON.stringify({ account_ids: accountIds }) + }) +} + +const removeAccountsFromList = ({ id, accountIds, credentials }) => { + const url = MASTODON_LIST_ACCOUNTS_URL(id) + const headers = authHeaders(credentials) + headers['Content-Type'] = 'application/json' + + return fetch(url, { + headers, + method: 'DELETE', + body: JSON.stringify({ account_ids: accountIds }) + }) +} + +const deleteList = ({ id, credentials }) => { + const url = MASTODON_LIST_URL(id) + return fetch(url, { + method: 'DELETE', + headers: authHeaders(credentials) + }) +} + const fetchConversation = ({ id, credentials }) => { const urlContext = MASTODON_STATUS_CONTEXT_URL(id) return fetch(urlContext, { headers: authHeaders(credentials) }) @@ -506,6 +585,7 @@ const fetchTimeline = ({ since = false, until = false, userId = false, + listId = false, tag = false, withMuted = false, replyVisibility = 'all' @@ -518,6 +598,7 @@ const fetchTimeline = ({ publicAndExternal: MASTODON_PUBLIC_TIMELINE, user: MASTODON_USER_TIMELINE_URL, media: MASTODON_USER_TIMELINE_URL, + list: MASTODON_LIST_TIMELINE_URL, favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, tag: MASTODON_TAG_TIMELINE_URL, bookmarks: MASTODON_BOOKMARK_TIMELINE_URL @@ -531,6 +612,10 @@ const fetchTimeline = ({ url = url(userId) } + if (timeline === 'list') { + url = url(listId) + } + if (since) { params.push(['since_id', since]) } @@ -1405,6 +1490,14 @@ const apiService = { addBackup, listBackups, fetchFollowRequests, + fetchLists, + createList, + getList, + updateList, + getListAccounts, + addAccountsToList, + removeAccountsFromList, + deleteList, approveUser, denyUser, suggestions, diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 4a40f5b5..62ee8549 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -2,10 +2,11 @@ import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.servic import timelineFetcher from '../timeline_fetcher/timeline_fetcher.service.js' import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js' import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service' +import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js' const backendInteractorService = credentials => ({ - startFetchingTimeline ({ timeline, store, userId = false, tag }) { - return timelineFetcher.startFetching({ timeline, store, credentials, userId, tag }) + startFetchingTimeline ({ timeline, store, userId = false, listId = false, tag }) { + return timelineFetcher.startFetching({ timeline, store, credentials, userId, listId, tag }) }, fetchTimeline (args) { @@ -24,6 +25,10 @@ const backendInteractorService = credentials => ({ return followRequestFetcher.startFetching({ store, credentials }) }, + startFetchingLists ({ store }) { + return listsFetcher.startFetching({ store, credentials }) + }, + startUserSocket ({ store }) { const serv = store.rootState.instance.server.replace('http', 'ws') const url = serv + getMastodonSocketURI({ credentials, stream: 'user' }) diff --git a/src/services/lists_fetcher/lists_fetcher.service.js b/src/services/lists_fetcher/lists_fetcher.service.js new file mode 100644 index 00000000..8d9dae66 --- /dev/null +++ b/src/services/lists_fetcher/lists_fetcher.service.js @@ -0,0 +1,22 @@ +import apiService from '../api/api.service.js' +import { promiseInterval } from '../promise_interval/promise_interval.js' + +const fetchAndUpdate = ({ store, credentials }) => { + return apiService.fetchLists({ credentials }) + .then(lists => { + store.commit('setLists', lists) + }, () => {}) + .catch(() => {}) +} + +const startFetching = ({ credentials, store }) => { + const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store }) + boundFetchAndUpdate() + return promiseInterval(boundFetchAndUpdate, 240000) +} + +const listsFetcher = { + startFetching +} + +export default listsFetcher diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 7ba138e0..8501907e 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -3,12 +3,13 @@ import { camelCase } from 'lodash' import apiService from '../api/api.service.js' import { promiseInterval } from '../promise_interval/promise_interval.js' -const update = ({ store, statuses, timeline, showImmediately, userId, pagination }) => { +const update = ({ store, statuses, timeline, showImmediately, userId, listId, pagination }) => { const ccTimeline = camelCase(timeline) store.dispatch('addNewStatuses', { timeline: ccTimeline, userId, + listId, statuses, showImmediately, pagination @@ -22,6 +23,7 @@ const fetchAndUpdate = ({ older = false, showImmediately = false, userId = false, + listId = false, tag = false, until, since @@ -44,6 +46,7 @@ const fetchAndUpdate = ({ } args.userId = userId + args.listId = listId args.tag = tag args.withMuted = !hideMutedPosts if (loggedIn && ['friends', 'public', 'publicAndExternal'].includes(timeline)) { @@ -62,7 +65,7 @@ const fetchAndUpdate = ({ if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) { store.dispatch('queueFlush', { timeline, id: timelineData.maxId }) } - update({ store, statuses, timeline, showImmediately, userId, pagination }) + update({ store, statuses, timeline, showImmediately, userId, listId, pagination }) return { statuses, pagination } }) .catch((error) => { @@ -75,14 +78,15 @@ const fetchAndUpdate = ({ }) } -const startFetching = ({ timeline = 'friends', credentials, store, userId = false, tag = false }) => { +const startFetching = ({ timeline = 'friends', credentials, store, userId = false, listId = false, tag = false }) => { const rootState = store.rootState || store.state const timelineData = rootState.statuses.timelines[camelCase(timeline)] const showImmediately = timelineData.visibleStatuses.length === 0 timelineData.userId = userId - fetchAndUpdate({ timeline, credentials, store, showImmediately, userId, tag }) + timelineData.listId = listId + fetchAndUpdate({ timeline, credentials, store, showImmediately, userId, listId, tag }) const boundFetchAndUpdate = () => - fetchAndUpdate({ timeline, credentials, store, userId, tag }) + fetchAndUpdate({ timeline, credentials, store, userId, listId, tag }) return promiseInterval(boundFetchAndUpdate, 10000) } const timelineFetcher = { diff --git a/test/unit/specs/boot/routes.spec.js b/test/unit/specs/boot/routes.spec.js index 5cffefbb..4c387567 100644 --- a/test/unit/specs/boot/routes.spec.js +++ b/test/unit/specs/boot/routes.spec.js @@ -40,4 +40,28 @@ describe('routes', () => { // eslint-disable-next-line no-prototype-builtins expect(matchedComponents[0].components.default.components.hasOwnProperty('UserCard')).to.eql(true) }) + + it('list view', async () => { + await router.push('/lists') + + const matchedComponents = router.currentRoute.value.matched + + expect(matchedComponents[0].components.default.components.hasOwnProperty('ListsCard')).to.eql(true) + }) + + it('list timeline', async () => { + await router.push('/lists/1') + + const matchedComponents = router.currentRoute.value.matched + + expect(matchedComponents[0].components.default.components.hasOwnProperty('Timeline')).to.eql(true) + }) + + it('list edit', async () => { + await router.push('/lists/1/edit') + + const matchedComponents = router.currentRoute.value.matched + + expect(matchedComponents[0].components.default.components.hasOwnProperty('BasicUserCard')).to.eql(true) + }) }) diff --git a/test/unit/specs/modules/lists.spec.js b/test/unit/specs/modules/lists.spec.js new file mode 100644 index 00000000..ac9af1b6 --- /dev/null +++ b/test/unit/specs/modules/lists.spec.js @@ -0,0 +1,83 @@ +import { cloneDeep } from 'lodash' +import { defaultState, mutations, getters } from '../../../../src/modules/lists.js' + +describe('The lists module', () => { + describe('mutations', () => { + it('updates array of all lists', () => { + const state = cloneDeep(defaultState) + const list = { id: '1', title: 'testList' } + + mutations.setLists(state, [list]) + expect(state.allLists).to.have.length(1) + expect(state.allLists).to.eql([list]) + }) + + it('adds a new list with a title, updating the title for existing lists', () => { + const state = cloneDeep(defaultState) + const list = { id: '1', title: 'testList' } + const modList = { id: '1', title: 'anotherTestTitle' } + + mutations.setList(state, list) + expect(state.allListsObject[list.id]).to.eql({ title: list.title }) + expect(state.allLists).to.have.length(1) + expect(state.allLists[0]).to.eql(list) + + mutations.setList(state, modList) + expect(state.allListsObject[modList.id]).to.eql({ title: modList.title }) + expect(state.allLists).to.have.length(1) + expect(state.allLists[0]).to.eql(modList) + }) + + it('adds a new list with an array of IDs, updating the IDs for existing lists', () => { + const state = cloneDeep(defaultState) + const list = { id: '1', accountIds: ['1', '2', '3'] } + const modList = { id: '1', accountIds: ['3', '4', '5'] } + + mutations.setListAccounts(state, list) + expect(state.allListsObject[list.id]).to.eql({ accountIds: list.accountIds }) + + mutations.setListAccounts(state, modList) + expect(state.allListsObject[modList.id]).to.eql({ accountIds: modList.accountIds }) + }) + + it('deletes a list', () => { + const state = { + allLists: [{ id: '1', title: 'testList' }], + allListsObject: { + 1: { title: 'testList', accountIds: ['1', '2', '3'] } + } + } + const id = '1' + + mutations.deleteList(state, { id }) + expect(state.allLists).to.have.length(0) + expect(state.allListsObject).to.eql({}) + }) + }) + + describe('getters', () => { + it('returns list title', () => { + const state = { + allLists: [{ id: '1', title: 'testList' }], + allListsObject: { + 1: { title: 'testList', accountIds: ['1', '2', '3'] } + } + } + const id = '1' + + expect(getters.findListTitle(state)(id)).to.eql('testList') + }) + + it('returns list accounts', () => { + const state = { + allLists: [{ id: '1', title: 'testList' }], + allListsObject: { + 1: { title: 'testList', accountIds: ['1', '2', '3'] } + } + } + const id = '1' + + expect(getters.findListAccounts(state)(id)).to.eql(['1', '2', '3']) + }) + }) +}) -- cgit v1.2.3-70-g09d2 From 4803fb07c85914fd0c9756a1d399e9803f48b8c7 Mon Sep 17 00:00:00 2001 From: Alexander Tumin Date: Mon, 8 Aug 2022 23:42:22 +0300 Subject: Allow opening profile when clicking an avatar inside of user popover --- src/components/settings_modal/tabs/general_tab.js | 5 +++++ src/components/settings_modal/tabs/general_tab.vue | 11 +++++++---- src/components/user_popover/user_popover.js | 4 ++-- src/components/user_popover/user_popover.vue | 2 +- src/i18n/en.json | 5 ++++- src/modules/config.js | 2 +- 6 files changed, 20 insertions(+), 9 deletions(-) (limited to 'src/modules/config.js') diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js index 1e11b9e0..d94a3be9 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -43,6 +43,11 @@ const GeneralTab = { value: mode, label: this.$t(`settings.third_column_mode_${mode}`) })), + userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map(mode => ({ + key: mode, + value: mode, + label: this.$t(`settings.user_popover_avatar_action_${mode}`) + })), loopSilentAvailable: // Firefox Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index a2609200..7ac61b2e 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -75,12 +75,15 @@
  • - - {{ $t('settings.user_popover_avatar_zoom') }} - + {{ $t('settings.user_popover_avatar_action') }} +
  • import('../popover/popover.vue')) }, computed: { - userPopoverZoom () { - return this.$store.getters.mergedConfig.userPopoverZoom + userPopoverAvatarAction () { + return this.$store.getters.mergedConfig.userPopoverAvatarAction }, userPopoverOverlay () { return this.$store.getters.mergedConfig.userPopoverOverlay diff --git a/src/components/user_popover/user_popover.vue b/src/components/user_popover/user_popover.vue index 4e999672..53d51fc4 100644 --- a/src/components/user_popover/user_popover.vue +++ b/src/components/user_popover/user_popover.vue @@ -14,7 +14,7 @@ class="user-popover" :user-id="userId" :hide-bio="true" - :avatar-action="userPopoverZoom ? 'zoom' : close" + :avatar-action="userPopoverAvatarAction == 'close' ? close : userPopoverAvatarAction" :on-close="close" /> diff --git a/src/i18n/en.json b/src/i18n/en.json index c54d4750..140e569d 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -556,7 +556,10 @@ "mention_link_show_avatar_quick": "Show user avatar next to mentions", "mention_link_fade_domain": "Fade domains (e.g. {'@'}example.org in {'@'}foo{'@'}example.org)", "mention_link_bolden_you": "Highlight mention of you when you are mentioned", - "user_popover_avatar_zoom": "Clicking on user avatar in popover zooms it instead of closing the popover", + "user_popover_avatar_action": "Popover avatar click action", + "user_popover_avatar_action_zoom": "Zoom the avatar", + "user_popover_avatar_action_close": "Close the popover", + "user_popover_avatar_action_open": "Open profile", "user_popover_avatar_overlay": "Show user popover over user avatar", "fun": "Fun", "greentext": "Meme arrows", diff --git a/src/modules/config.js b/src/modules/config.js index eaf67a91..e1e0bdd7 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -81,7 +81,7 @@ export const defaultState = { useContainFit: true, disableStickyHeaders: false, showScrollbars: false, - userPopoverZoom: false, + userPopoverAvatarAction: 'close', userPopoverOverlay: true, greentext: undefined, // instance default useAtIcon: undefined, // instance default -- cgit v1.2.3-70-g09d2 From a403f93b478eec67d779400b1fcfdc006bb787da Mon Sep 17 00:00:00 2001 From: Alexander Tumin Date: Tue, 9 Aug 2022 10:44:20 +0300 Subject: Allow opening profile: multiChoiceProprties record, anonymous access --- src/components/settings_modal/tabs/general_tab.vue | 1 - src/modules/config.js | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/modules/config.js') diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 7ac61b2e..c212a81a 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -76,7 +76,6 @@
  • Date: Sun, 5 Jun 2022 17:10:44 +0300 Subject: Allow column width configuration Group column configuration in settings Column width configuration: do not act on defaults --- src/App.scss | 19 +++++- src/boot/after_store.js | 4 +- .../settings_modal/helpers/boolean_setting.js | 3 + .../settings_modal/helpers/boolean_setting.vue | 7 ++- .../settings_modal/helpers/choice_setting.js | 3 + .../settings_modal/helpers/choice_setting.vue | 5 +- .../settings_modal/helpers/integer_setting.js | 3 + .../settings_modal/helpers/integer_setting.vue | 5 +- .../settings_modal/helpers/size_setting.js | 67 ++++++++++++++++++++++ .../settings_modal/helpers/size_setting.vue | 54 +++++++++++++++++ src/components/settings_modal/tabs/general_tab.js | 16 ++++++ src/components/settings_modal/tabs/general_tab.vue | 38 +++++------- src/i18n/en.json | 5 ++ src/i18n/ru.json | 9 +++ src/modules/config.js | 9 ++- src/services/style_setter/style_setter.js | 31 ++++++++++ 16 files changed, 244 insertions(+), 34 deletions(-) create mode 100644 src/components/settings_modal/helpers/size_setting.js create mode 100644 src/components/settings_modal/helpers/size_setting.vue (limited to 'src/modules/config.js') diff --git a/src/App.scss b/src/App.scss index ab025d63..0aa28933 100644 --- a/src/App.scss +++ b/src/App.scss @@ -186,9 +186,13 @@ nav { --columnGap: 1em; --status-margin: 0.75em; + --effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn))); + position: relative; display: grid; - grid-template-columns: var(--miniColumn) var(--maxiColumn); + grid-template-columns: + var(--sidebarColumnWidth, var(--miniColumn)) + var(--effectiveContentColumnWidth); grid-template-areas: "sidebar content"; grid-template-rows: 1fr; box-sizing: border-box; @@ -282,15 +286,24 @@ nav { } &.-reverse:not(.-wide):not(.-mobile) { - grid-template-columns: var(--maxiColumn) var(--miniColumn); + grid-template-columns: + var(--effectiveContentColumnWidth) + minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn))); grid-template-areas: "content sidebar"; } &.-wide { - grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn); + grid-template-columns: + minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn))) + var(--effectiveContentColumnWidth) + minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn))); grid-template-areas: "sidebar content notifs"; &.-reverse { + grid-template-columns: + minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn))) + var(--effectiveContentColumnWidth) + minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn))); grid-template-areas: "notifs content sidebar"; } } diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 908d905a..38b5f38e 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -12,7 +12,7 @@ import { windowWidth, windowHeight } from '../services/window_utils/window_utils import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' -import { applyTheme } from '../services/style_setter/style_setter.js' +import { applyTheme, applyConfig } from '../services/style_setter/style_setter.js' import FaviconService from '../services/favicon_service/favicon_service.js' let staticInitialResults = null @@ -360,6 +360,8 @@ const afterStoreSetup = async ({ store, i18n }) => { console.error('Failed to load any theme!') } + applyConfig(store.state.config) + // Now we can try getting the server settings and logging in // Most of these are preloaded into the index.html so blocking is minimized await Promise.all([ diff --git a/src/components/settings_modal/helpers/boolean_setting.js b/src/components/settings_modal/helpers/boolean_setting.js index 353e551c..dc832044 100644 --- a/src/components/settings_modal/helpers/boolean_setting.js +++ b/src/components/settings_modal/helpers/boolean_setting.js @@ -42,6 +42,9 @@ export default { methods: { update (e) { set(this.$parent, this.path, e) + }, + reset () { + set(this.$parent, this.path, this.defaultState) } } } diff --git a/src/components/settings_modal/helpers/boolean_setting.vue b/src/components/settings_modal/helpers/boolean_setting.vue index 69584808..41142966 100644 --- a/src/components/settings_modal/helpers/boolean_setting.vue +++ b/src/components/settings_modal/helpers/boolean_setting.vue @@ -15,7 +15,12 @@ {{ ' ' }} - + + + diff --git a/src/components/settings_modal/helpers/choice_setting.js b/src/components/settings_modal/helpers/choice_setting.js index 4677d4c1..3da559fe 100644 --- a/src/components/settings_modal/helpers/choice_setting.js +++ b/src/components/settings_modal/helpers/choice_setting.js @@ -43,6 +43,9 @@ export default { methods: { update (e) { set(this.$parent, this.path, e) + }, + reset () { + set(this.$parent, this.path, this.defaultState) } } } diff --git a/src/components/settings_modal/helpers/choice_setting.vue b/src/components/settings_modal/helpers/choice_setting.vue index 258c7422..d141a0d6 100644 --- a/src/components/settings_modal/helpers/choice_setting.vue +++ b/src/components/settings_modal/helpers/choice_setting.vue @@ -19,7 +19,10 @@ {{ option.value === defaultState ? $t('settings.instance_default_simple') : '' }} - + diff --git a/src/components/settings_modal/helpers/integer_setting.js b/src/components/settings_modal/helpers/integer_setting.js index 17dc0e7b..e64d0cee 100644 --- a/src/components/settings_modal/helpers/integer_setting.js +++ b/src/components/settings_modal/helpers/integer_setting.js @@ -36,6 +36,9 @@ export default { methods: { update (e) { set(this.$parent, this.path, parseInt(e.target.value)) + }, + reset () { + set(this.$parent, this.path, this.defaultState) } } } diff --git a/src/components/settings_modal/helpers/integer_setting.vue b/src/components/settings_modal/helpers/integer_setting.vue index e661a025..695e2673 100644 --- a/src/components/settings_modal/helpers/integer_setting.vue +++ b/src/components/settings_modal/helpers/integer_setting.vue @@ -17,7 +17,10 @@ @change="update" > {{ ' ' }} - + diff --git a/src/components/settings_modal/helpers/size_setting.js b/src/components/settings_modal/helpers/size_setting.js new file mode 100644 index 00000000..58697412 --- /dev/null +++ b/src/components/settings_modal/helpers/size_setting.js @@ -0,0 +1,67 @@ +import { get, set } from 'lodash' +import ModifiedIndicator from './modified_indicator.vue' +import Select from 'src/components/select/select.vue' + +export const allCssUnits = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%'] +export const defaultHorizontalUnits = ['px', 'rem', 'vw'] +export const defaultVerticalUnits = ['px', 'rem', 'vh'] + +export default { + components: { + ModifiedIndicator, + Select + }, + props: { + path: String, + disabled: Boolean, + min: Number, + units: { + type: [String], + default: () => allCssUnits + }, + expert: [Number, String] + }, + computed: { + pathDefault () { + const [firstSegment, ...rest] = this.path.split('.') + return [firstSegment + 'DefaultValue', ...rest].join('.') + }, + stateUnit () { + return (this.state || '').replace(/\d+/, '') + }, + stateValue () { + return (this.state || '').replace(/\D+/, '') + }, + state () { + const value = get(this.$parent, this.path) + if (value === undefined) { + return this.defaultState + } else { + return value + } + }, + defaultState () { + return get(this.$parent, this.pathDefault) + }, + isChanged () { + return this.state !== this.defaultState + }, + matchesExpertLevel () { + return (this.expert || 0) <= this.$parent.expertLevel + } + }, + methods: { + update (e) { + set(this.$parent, this.path, e) + }, + reset () { + set(this.$parent, this.path, this.defaultState) + }, + updateValue (e) { + set(this.$parent, this.path, parseInt(e.target.value) + this.stateUnit) + }, + updateUnit (e) { + set(this.$parent, this.path, this.stateValue + e.target.value) + } + } +} diff --git a/src/components/settings_modal/helpers/size_setting.vue b/src/components/settings_modal/helpers/size_setting.vue new file mode 100644 index 00000000..90c9f538 --- /dev/null +++ b/src/components/settings_modal/helpers/size_setting.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js index 1e11b9e0..a22b9b03 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -2,6 +2,7 @@ import BooleanSetting from '../helpers/boolean_setting.vue' import ChoiceSetting from '../helpers/choice_setting.vue' import ScopeSelector from 'src/components/scope_selector/scope_selector.vue' import IntegerSetting from '../helpers/integer_setting.vue' +import SizeSetting, { defaultHorizontalUnits } from '../helpers/size_setting.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' @@ -56,11 +57,15 @@ const GeneralTab = { BooleanSetting, ChoiceSetting, IntegerSetting, + SizeSetting, InterfaceLanguageSwitcher, ScopeSelector, ServerSideIndicator }, computed: { + horizontalUnits () { + return defaultHorizontalUnits + }, postFormats () { return this.$store.state.instance.postFormats || [] }, @@ -71,6 +76,17 @@ const GeneralTab = { label: this.$t(`post_status.content_type["${format}"]`) })) }, + columns () { + const mode = this.$store.getters.mergedConfig.thirdColumnMode + + const notif = mode === 'none' ? [] : ['notifs'] + + if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') { + return [...notif, 'content', 'sidebar'] + } else { + return ['sidebar', 'content', ...notif] + } + }, instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }, instanceWallpaperUsed () { return this.$store.state.instance.background && diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 91015955..db321363 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -15,11 +15,6 @@ {{ $t('settings.hide_isp') }}
  • -
  • - - {{ $t('settings.right_sidebar') }} - -
  • {{ $t('settings.hide_wallpaper') }} @@ -64,16 +59,6 @@ {{ $t('settings.virtual_scrolling') }}
  • -
  • - - {{ $t('settings.disable_sticky_headers') }} - -
  • -
  • - - {{ $t('settings.show_scrollbars') }} - -
  • -
  • - - {{ $t('settings.third_column_mode') }} - -
  • + + diff --git a/src/i18n/en.json b/src/i18n/en.json index 2e845959..7e957ffd 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -533,6 +533,11 @@ "third_column_mode_none": "Don't show third column at all", "third_column_mode_notifications": "Notifications column", "third_column_mode_postform": "Main post form and navigation", + "columns": "Columns", + "column_sizes": "Column sizes", + "column_sizes_sidebar": "Sidebar", + "column_sizes_content": "Content", + "column_sizes_notifs": "Notifications", "tree_advanced": "Allow more flexible navigation in tree view", "tree_fade_ancestors": "Display ancestors of the current status in faint text", "conversation_display_linear": "Linear-style", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 7e6ff3f5..02815f3e 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -456,6 +456,15 @@ "subject_line_mastodon": "Как в Mastodon: скопировать как есть", "subject_line_email": "Как в электронной почте: \"re: тема\"", "subject_line_behavior": "Копировать тему в ответах", + "third_column_mode": "Когда недостаточно места, показывать третью колонку содержащую", + "third_column_mode_none": "Не показывать третью колонку совсем", + "third_column_mode_notifications": "Колонку уведомлений", + "third_column_mode_postform": "Форму отправки сообщения и навигацию", + "columns": "Колонки", + "column_sizes": "Размеры колонок", + "column_sizes_sidebar": "Боковой", + "column_sizes_content": "Содержимого", + "column_sizes_notifs": "Уведомлений", "no_mutes": "Нет игнорируемых", "no_blocks": "Нет блокировок", "notification_visibility_emoji_reactions": "Реакции", diff --git a/src/modules/config.js b/src/modules/config.js index c34b2c8c..2918f865 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -1,5 +1,5 @@ import Cookies from 'js-cookie' -import { setPreset, applyTheme } from '../services/style_setter/style_setter.js' +import { setPreset, applyTheme, applyConfig } from '../services/style_setter/style_setter.js' import messages from '../i18n/messages' import localeService from '../services/locale/locale.service.js' @@ -165,12 +165,17 @@ const config = { setHighlight ({ commit, dispatch }, { user, color, type }) { commit('setHighlight', { user, color, type }) }, - setOption ({ commit, dispatch }, { name, value }) { + setOption ({ commit, dispatch, state }, { name, value }) { commit('setOption', { name, value }) switch (name) { case 'theme': setPreset(value) break + case 'sidebarColumnWidth': + case 'contentColumnWidth': + case 'notifsColumnWidth': + applyConfig(state) + break case 'customTheme': case 'customThemeSource': applyTheme(value) diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 543aa874..d6e973a1 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -1,6 +1,7 @@ import { convert } from 'chromatism' import { rgb2hex, hex2rgb, rgba2css, getCssColor, relativeLuminance } from '../color_convert/color_convert.js' import { getColors, computeDynamicColor, getOpacitySlot } from '../theme_data/theme_data.service.js' +import { defaultState } from '../../modules/config.js' export const applyTheme = (input) => { const { rules } = generatePreset(input) @@ -20,6 +21,36 @@ export const applyTheme = (input) => { body.classList.remove('hidden') } +const configColumns = ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth }) => + ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth }) + +const defaultConfigColumns = configColumns(defaultState) + +export const applyConfig = (config) => { + const columns = configColumns(config) + + if (columns === defaultConfigColumns) { + return + } + + const head = document.head + const body = document.body + body.classList.add('hidden') + + const rules = Object + .entries(columns) + .filter(([k, v]) => v) + .map(([k, v]) => `--${k}: ${v}`).join(';') + + const styleEl = document.createElement('style') + head.appendChild(styleEl) + const styleSheet = styleEl.sheet + + styleSheet.toString() + styleSheet.insertRule(`:root { ${rules} }`, 'index-max') + body.classList.remove('hidden') +} + export const getCssShadow = (input, usesDropShadow) => { if (input.length === 0) { return 'none' -- cgit v1.2.3-70-g09d2 From a29835375a62549410a7df7922f8eb3f9b391487 Mon Sep 17 00:00:00 2001 From: Alexander Tumin Date: Wed, 17 Aug 2022 02:33:39 +0300 Subject: Allow column width configuration: allow stretching navbar with columns --- src/App.js | 7 +++++++ src/App.scss | 15 ++++++++------- src/App.vue | 5 ++++- src/components/desktop_nav/desktop_nav.scss | 20 ++++++++++++++++++++ src/components/settings_modal/tabs/general_tab.vue | 5 +++++ src/i18n/en.json | 1 + src/modules/config.js | 1 + 7 files changed, 46 insertions(+), 8 deletions(-) (limited to 'src/modules/config.js') diff --git a/src/App.js b/src/App.js index f5bd7e2e..d1ad16d5 100644 --- a/src/App.js +++ b/src/App.js @@ -60,6 +60,13 @@ export default { '-' + this.layoutType ] }, + navClasses () { + const { navbarColumnStretch } = this.$store.getters.mergedConfig + return [ + '-' + this.layoutType, + ...(navbarColumnStretch ? ['-column-stretch'] : []) + ] + }, currentUser () { return this.$store.state.users.currentUser }, userBackground () { return this.currentUser.background_image }, instanceBackground () { diff --git a/src/App.scss b/src/App.scss index 3c16007e..02f5e049 100644 --- a/src/App.scss +++ b/src/App.scss @@ -185,13 +185,14 @@ nav { --maxiColumn: 45rem; --columnGap: 1em; --status-margin: 0.75em; - + --effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn))); + --effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn))); --effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn))); position: relative; display: grid; grid-template-columns: - minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn))) + var(--effectiveSidebarColumnWidth) var(--effectiveContentColumnWidth); grid-template-areas: "sidebar content"; grid-template-rows: 1fr; @@ -288,22 +289,22 @@ nav { &.-reverse:not(.-wide):not(.-mobile) { grid-template-columns: var(--effectiveContentColumnWidth) - minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn))); + var(--effectiveSidebarColumnWidth); grid-template-areas: "content sidebar"; } &.-wide { grid-template-columns: - minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn))) + var(--effectiveSidebarColumnWidth) var(--effectiveContentColumnWidth) - minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn))); + var(--effectiveNotifsColumnWidth); grid-template-areas: "sidebar content notifs"; &.-reverse { grid-template-columns: - minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn))) + var(--effectiveNotifsColumnWidth) var(--effectiveContentColumnWidth) - minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn))); + var(--effectiveSidebarColumnWidth); grid-template-areas: "notifs content sidebar"; } } diff --git a/src/App.vue b/src/App.vue index c741aa70..1f96efe8 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,7 +8,10 @@ class="app-bg-wrapper" /> - +
  • +
  • + + {{ $t('settings.navbar_column_stretch') }} + +