diff --git a/src/components/lists/lists.js b/src/components/lists/lists.js index 56d68430..b3527dc1 100644 --- a/src/components/lists/lists.js +++ b/src/components/lists/lists.js @@ -1,3 +1,4 @@ +import { useListsStore } from '../../stores/lists' import ListsCard from '../lists_card/lists_card.vue' const Lists = { @@ -11,7 +12,7 @@ const Lists = { }, computed: { lists () { - return this.$store.state.lists.allLists + return useListsStore().allLists } }, methods: { diff --git a/src/components/lists_edit/lists_edit.js b/src/components/lists_edit/lists_edit.js index d929dbdf..9980511a 100644 --- a/src/components/lists_edit/lists_edit.js +++ b/src/components/lists_edit/lists_edit.js @@ -1,4 +1,5 @@ import { mapState, mapGetters } from 'vuex' +import { mapState as mapPiniaState } from 'pinia' 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' @@ -10,6 +11,7 @@ import { faChevronLeft } from '@fortawesome/free-solid-svg-icons' import { useInterfaceStore } from '../../stores/interface' +import { useListsStore } from '../../stores/lists' library.add( faSearch, @@ -38,12 +40,12 @@ const ListsNew = { }, created () { if (!this.id) return - this.$store.dispatch('fetchList', { listId: this.id }) + useListsStore().fetchList({ listId: this.id }) .then(() => { this.title = this.findListTitle(this.id) this.titleDraft = this.title }) - this.$store.dispatch('fetchListAccounts', { listId: this.id }) + useListsStore().fetchListAccounts({ listId: this.id }) .then(() => { this.membersUserIds = this.findListAccounts(this.id) this.membersUserIds.forEach(userId => { @@ -65,7 +67,8 @@ const ListsNew = { ...mapState({ currentUser: state => state.users.currentUser }), - ...mapGetters(['findUser', 'findListTitle', 'findListAccounts']) + ...mapPiniaState(useListsStore, ['findListTitle', 'findListAccounts']), + ...mapGetters(['findUser']) }, methods: { onInput () { @@ -96,10 +99,10 @@ const ListsNew = { return this.addedUserIds.has(user.id) }, addUser (user) { - this.$store.dispatch('addListAccount', { accountId: user.id, listId: this.id }) + useListsStore().addListAccount({ accountId: user.id, listId: this.id }) }, removeUser (userId) { - this.$store.dispatch('removeListAccount', { accountId: userId, listId: this.id }) + useListsStore().removeListAccount({ accountId: userId, listId: this.id }) }, onSearchLoading (results) { this.searchLoading = true @@ -112,17 +115,16 @@ const ListsNew = { this.searchUserIds = results }, updateListTitle () { - this.$store.dispatch('setList', { listId: this.id, title: this.titleDraft }) + useListsStore().setList({ listId: this.id, title: this.titleDraft }) .then(() => { this.title = this.findListTitle(this.id) }) }, createList () { - this.$store.dispatch('createList', { title: this.titleDraft }) + useListsStore().createList({ title: this.titleDraft }) .then((list) => { - return this - .$store - .dispatch('setListAccounts', { listId: list.id, accountIds: [...this.addedUserIds] }) + return useListsStore() + .setListAccounts({ listId: list.id, accountIds: [...this.addedUserIds] }) .then(() => list.id) }) .then((listId) => { @@ -137,7 +139,7 @@ const ListsNew = { }) }, deleteList () { - this.$store.dispatch('deleteList', { listId: this.id }) + useListsStore().deleteList({ listId: this.id }) this.$router.push({ name: 'lists' }) } } diff --git a/src/components/lists_menu/lists_menu_content.js b/src/components/lists_menu/lists_menu_content.js index 97b32210..d941127c 100644 --- a/src/components/lists_menu/lists_menu_content.js +++ b/src/components/lists_menu/lists_menu_content.js @@ -1,6 +1,8 @@ import { mapState } from 'vuex' +import { mapState as mapPiniaState } from 'pinia' import NavigationEntry from 'src/components/navigation/navigation_entry.vue' import { getListEntries } from 'src/components/navigation/filter.js' +import { useListsStore } from '../../stores/lists' export const ListsMenuContent = { props: [ @@ -10,8 +12,10 @@ export const ListsMenuContent = { NavigationEntry }, computed: { + ...mapPiniaState(useListsStore, { + lists: getListEntries + }), ...mapState({ - lists: getListEntries, currentUser: state => state.users.currentUser, privateMode: state => state.instance.private, federating: state => state.instance.federating diff --git a/src/components/lists_timeline/lists_timeline.js b/src/components/lists_timeline/lists_timeline.js index c3f408bd..8c13d5b5 100644 --- a/src/components/lists_timeline/lists_timeline.js +++ b/src/components/lists_timeline/lists_timeline.js @@ -1,3 +1,4 @@ +import { useListsStore } from '../../stores/lists' import Timeline from '../timeline/timeline.vue' const ListsTimeline = { data () { @@ -17,14 +18,14 @@ const ListsTimeline = { this.listId = route.params.id this.$store.dispatch('stopFetchingTimeline', 'list') this.$store.commit('clearTimeline', { timeline: 'list' }) - this.$store.dispatch('fetchList', { listId: this.listId }) + useListsStore().fetchList({ listId: this.listId }) this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId }) } } }, created () { this.listId = this.$route.params.id - this.$store.dispatch('fetchList', { listId: this.listId }) + useListsStore().fetchList({ listId: this.listId }) this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId }) }, unmounted () { diff --git a/src/components/navigation/filter.js b/src/components/navigation/filter.js index e8e77f8f..68f9f0d8 100644 --- a/src/components/navigation/filter.js +++ b/src/components/navigation/filter.js @@ -11,7 +11,7 @@ export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFede }) } -export const getListEntries = state => state.lists.allLists.map(list => ({ +export const getListEntries = store => store.allLists.map(list => ({ name: 'list-' + list.id, routeObject: { name: 'lists-timeline', params: { id: list.id } }, labelRaw: list.title, diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js index ef78e44c..5001a8c3 100644 --- a/src/components/navigation/navigation_pins.js +++ b/src/components/navigation/navigation_pins.js @@ -1,4 +1,5 @@ import { mapState } from 'vuex' +import { mapState as mapPiniaState } from 'pinia' import { TIMELINES, ROOT_ITEMS, routeTo } from 'src/components/navigation/navigation.js' import { getListEntries, filterNavigation } from 'src/components/navigation/filter.js' @@ -14,6 +15,7 @@ import { faStream, faList } from '@fortawesome/free-solid-svg-icons' +import { useListsStore } from '../../stores/lists' library.add( faUsers, @@ -38,8 +40,10 @@ const NavPanel = { getters () { return this.$store.getters }, + ...mapPiniaState(useListsStore, { + lists: getListEntries + }), ...mapState({ - lists: getListEntries, currentUser: state => state.users.currentUser, followRequestCount: state => state.api.followRequests.length, privateMode: state => state.instance.private, diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js index a9e7893c..79c944b7 100644 --- a/src/components/timeline_menu/timeline_menu.js +++ b/src/components/timeline_menu/timeline_menu.js @@ -9,6 +9,7 @@ import { faChevronDown } from '@fortawesome/free-solid-svg-icons' import { useInterfaceStore } from '../../stores/interface' +import { useListsStore } from '../../stores/lists' library.add(faChevronDown) @@ -87,7 +88,7 @@ const TimelineMenu = { return '#' + this.$route.params.tag } if (route === 'lists-timeline') { - return this.$store.getters.findListTitle(this.$route.params.id) + return useListsStore().findListTitle(this.$route.params.id) } const i18nkey = timelineNames()[this.$route.name] return i18nkey ? this.$t(i18nkey) : route diff --git a/src/components/user_list_menu/user_list_menu.js b/src/components/user_list_menu/user_list_menu.js index 21996031..7b2fa8c4 100644 --- a/src/components/user_list_menu/user_list_menu.js +++ b/src/components/user_list_menu/user_list_menu.js @@ -1,9 +1,10 @@ import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronRight } from '@fortawesome/free-solid-svg-icons' -import { mapState } from 'vuex' +import { mapState } from 'pinia' import DialogModal from '../dialog_modal/dialog_modal.vue' import Popover from '../popover/popover.vue' +import { useListsStore } from '../../stores/lists' library.add(faChevronRight) @@ -22,8 +23,8 @@ const UserListMenu = { this.$store.dispatch('fetchUserInLists', this.user.id) }, computed: { - ...mapState({ - allLists: state => state.lists.allLists + ...mapState(useListsStore, { + allLists: store => store.allLists }), inListsSet () { return new Set(this.user.inLists.map(x => x.id)) @@ -39,12 +40,12 @@ const UserListMenu = { methods: { toggleList (listId) { if (this.inListsSet.has(listId)) { - this.$store.dispatch('removeListAccount', { accountId: this.user.id, listId }).then((response) => { + useListsStore().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) => { + useListsStore().addListAccount({ accountId: this.user.id, listId }).then((response) => { if (!response.ok) { return } this.$store.dispatch('fetchUserInLists', this.user.id) }) diff --git a/src/main.js b/src/main.js index 383b7d98..4ebfb4ff 100644 --- a/src/main.js +++ b/src/main.js @@ -6,7 +6,6 @@ import './lib/event_target_polyfill.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' @@ -66,7 +65,6 @@ 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/services/lists_fetcher/lists_fetcher.service.js b/src/services/lists_fetcher/lists_fetcher.service.js index 8d9dae66..c0306085 100644 --- a/src/services/lists_fetcher/lists_fetcher.service.js +++ b/src/services/lists_fetcher/lists_fetcher.service.js @@ -1,10 +1,11 @@ +import { useListsStore } from '../../stores/lists.js' 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) + useListsStore().setLists(lists) }, () => {}) .catch(() => {}) } diff --git a/src/stores/lists.js b/src/stores/lists.js new file mode 100644 index 00000000..3d4dedbf --- /dev/null +++ b/src/stores/lists.js @@ -0,0 +1,116 @@ +import { defineStore } from 'pinia' + +import { remove, find } from 'lodash' + +export const defaultState = { + allLists: [], + allListsObject: {} +} + +export const getters = { + findListTitle (state) { + return (id) => { + if (!this.allListsObject[id]) return + return this.allListsObject[id].title + } + }, + findListAccounts (state) { + return (id) => [...this.allListsObject[id].accountIds] + } +} + +export const actions = { + setLists (value) { + this.allLists = value + }, + createList ({ title }) { + return window.vuex.state.api.backendInteractor.createList({ title }) + .then((list) => { + this.setList({ listId: list.id, title }) + return list + }) + }, + fetchList ({ listId }) { + return window.vuex.state.api.backendInteractor.getList({ listId }) + .then((list) => this.setList({ listId: list.id, title: list.title })) + }, + fetchListAccounts ({ listId }) { + return window.vuex.state.api.backendInteractor.getListAccounts({ listId }) + .then((accountIds) => { + if (!this.allListsObject[listId]) { + this.allListsObject[listId] = { accountIds: [] } + } + this.allListsObject[listId].accountIds = accountIds + }) + }, + setList ({ listId, title }) { + if (!this.allListsObject[listId]) { + this.allListsObject[listId] = { accountIds: [] } + } + this.allListsObject[listId].title = title + + const entry = find(this.allLists, { id: listId }) + if (!entry) { + this.allLists.push({ id: listId, title }) + } else { + entry.title = title + } + }, + setListAccounts ({ listId, accountIds }) { + const saved = this.allListsObject[listId].accountIds || [] + const added = accountIds.filter(id => !saved.includes(id)) + const removed = saved.filter(id => !accountIds.includes(id)) + if (!this.allListsObject[listId]) { + this.allListsObject[listId] = { accountIds: [] } + } + this.allListsObject[listId].accountIds = accountIds + if (added.length > 0) { + window.vuex.state.api.backendInteractor.addAccountsToList({ listId, accountIds: added }) + } + if (removed.length > 0) { + window.vuex.state.api.backendInteractor.removeAccountsFromList({ listId, accountIds: removed }) + } + }, + addListAccount ({ listId, accountId }) { + return window.vuex.state + .api + .backendInteractor + .addAccountsToList({ listId, accountIds: [accountId] }) + .then((result) => { + if (!this.allListsObject[listId]) { + this.allListsObject[listId] = { accountIds: [] } + } + this.allListsObject[listId].accountIds.push(accountId) + return result + }) + }, + removeListAccount ({ listId, accountId }) { + return window.vuex.state + .api + .backendInteractor + .removeAccountsFromList({ listId, accountIds: [accountId] }) + .then((result) => { + if (!this.allListsObject[listId]) { + this.allListsObject[listId] = { accountIds: [] } + } + const { accountIds } = this.allListsObject[listId] + const set = new Set(accountIds) + set.delete(accountId) + this.allListsObject[listId].accountIds = [...set] + + return result + }) + }, + deleteList ({ listId }) { + window.vuex.state.api.backendInteractor.deleteList({ listId }) + + delete this.allListsObject[listId] + remove(this.allLists, list => list.id === listId) + } +} + +export const useListsStore = defineStore('lists', { + state: () => (defaultState), + getters, + actions +}) diff --git a/test/unit/specs/modules/lists.spec.js b/test/unit/specs/modules/lists.spec.js deleted file mode 100644 index e43106ea..00000000 --- a/test/unit/specs/modules/lists.spec.js +++ /dev/null @@ -1,83 +0,0 @@ -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, { listId: list.id, title: list.title }) - expect(state.allListsObject[list.id]).to.eql({ title: list.title, accountIds: [] }) - expect(state.allLists).to.have.length(1) - expect(state.allLists[0]).to.eql(list) - - mutations.setList(state, { listId: modList.id, title: modList.title }) - expect(state.allListsObject[modList.id]).to.eql({ title: modList.title, accountIds: [] }) - expect(state.allLists).to.have.length(1) - expect(state.allLists[0]).to.eql(modList) - }) - - 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, { listId: list.id, accountIds: list.accountIds }) - expect(state.allListsObject[list.id]).to.eql({ accountIds: list.accountIds }) - - mutations.setListAccounts(state, { listId: modList.id, accountIds: modList.accountIds }) - 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 listId = '1' - - mutations.deleteList(state, { listId }) - 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']) - }) - }) -}) diff --git a/test/unit/specs/stores/lists.spec.js b/test/unit/specs/stores/lists.spec.js new file mode 100644 index 00000000..299d53a6 --- /dev/null +++ b/test/unit/specs/stores/lists.spec.js @@ -0,0 +1,93 @@ +import { createPinia, setActivePinia } from 'pinia' +import { useListsStore } from '../../../../src/stores/lists.js' +import { createStore } from 'vuex' +import apiModule from '../../../../src/modules/api.js' + +setActivePinia(createPinia()) +const store = useListsStore() +window.vuex = createStore({ + modules: { + api: apiModule + } +}) + +describe('The lists store', () => { + describe('actions', () => { + it('updates array of all lists', () => { + store.$reset() + const list = { id: '1', title: 'testList' } + + store.setLists([list]) + expect(store.allLists).to.have.length(1) + expect(store.allLists).to.eql([list]) + }) + + it('adds a new list with a title, updating the title for existing lists', () => { + store.$reset() + const list = { id: '1', title: 'testList' } + const modList = { id: '1', title: 'anotherTestTitle' } + + store.setList({ listId: list.id, title: list.title }) + expect(store.allListsObject[list.id]).to.eql({ title: list.title, accountIds: [] }) + expect(store.allLists).to.have.length(1) + expect(store.allLists[0]).to.eql(list) + + store.setList({ listId: modList.id, title: modList.title }) + expect(store.allListsObject[modList.id]).to.eql({ title: modList.title, accountIds: [] }) + expect(store.allLists).to.have.length(1) + expect(store.allLists[0]).to.eql(modList) + }) + + it('adds a new list with an array of IDs, updating the IDs for existing lists', () => { + store.$reset() + const list = { id: '1', accountIds: ['1', '2', '3'] } + const modList = { id: '1', accountIds: ['3', '4', '5'] } + + store.setListAccounts({ listId: list.id, accountIds: list.accountIds }) + expect(store.allListsObject[list.id].accountIds).to.eql(list.accountIds) + + store.setListAccounts({ listId: modList.id, accountIds: modList.accountIds }) + expect(store.allListsObject[modList.id].accountIds).to.eql(modList.accountIds) + }) + + it('deletes a list', () => { + store.$patch({ + allLists: [{ id: '1', title: 'testList' }], + allListsObject: { + 1: { title: 'testList', accountIds: ['1', '2', '3'] } + } + }) + const listId = '1' + + store.deleteList({ listId }) + expect(store.allLists).to.have.length(0) + expect(store.allListsObject).to.eql({}) + }) + }) + + describe('getters', () => { + it('returns list title', () => { + store.$patch({ + allLists: [{ id: '1', title: 'testList' }], + allListsObject: { + 1: { title: 'testList', accountIds: ['1', '2', '3'] } + } + }) + const id = '1' + + expect(store.findListTitle(id)).to.eql('testList') + }) + + it('returns list accounts', () => { + store.$patch({ + allLists: [{ id: '1', title: 'testList' }], + allListsObject: { + 1: { title: 'testList', accountIds: ['1', '2', '3'] } + } + }) + const id = '1' + + expect(store.findListAccounts(id)).to.eql(['1', '2', '3']) + }) + }) +})