From cf33b3295f9465dfe41719fbb5ca26ce11f2fe25 Mon Sep 17 00:00:00 2001 From: Sol Fisher Romanoff Date: Thu, 16 Jun 2022 05:41:43 +0300 Subject: [PATCH] Add ability to edit and delete lists --- src/boot/routes.js | 4 +- src/components/list_card/list_card.js | 9 +++ src/components/list_card/list_card.vue | 34 ++++++-- src/components/list_edit/list_edit.js | 106 +++++++++++++++++++++++++ src/components/list_edit/list_edit.vue | 106 +++++++++++++++++++++++++ src/i18n/en.json | 4 +- src/services/api/api.service.js | 38 +++++++++ 7 files changed, 291 insertions(+), 10 deletions(-) create mode 100644 src/components/list_edit/list_edit.js create mode 100644 src/components/list_edit/list_edit.vue diff --git a/src/boot/routes.js b/src/boot/routes.js index 715b394e..1ab8209d 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -22,6 +22,7 @@ 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 ListTimeline from 'components/list_timeline/list_timeline.vue' +import ListEdit from 'components/list_edit/list_edit.vue' export default (store) => { const validateAuthenticatedRoute = (to, from, next) => { @@ -73,7 +74,8 @@ export default (store) => { { name: 'about', path: '/about', component: About }, { name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile }, { name: 'lists', path: '/lists', component: Lists }, - { name: 'list-timeline', path: '/lists/:id', component: ListTimeline } + { name: 'list-timeline', path: '/lists/:id', component: ListTimeline }, + { name: 'list-edit', path: '/lists/:id/edit', component: ListEdit } ] if (store.state.instance.pleromaChatMessagesAvailable) { diff --git a/src/components/list_card/list_card.js b/src/components/list_card/list_card.js index 42e56aff..6546796c 100644 --- a/src/components/list_card/list_card.js +++ b/src/components/list_card/list_card.js @@ -1,3 +1,12 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEllipsisH +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faEllipsisH +) + const ListCard = { props: [ 'list' diff --git a/src/components/list_card/list_card.vue b/src/components/list_card/list_card.vue index 5b4fefc8..752a7eb5 100644 --- a/src/components/list_card/list_card.vue +++ b/src/components/list_card/list_card.vue @@ -1,10 +1,21 @@ @@ -13,10 +24,13 @@ @import '../../_variables.scss'; .list-card { + display: flex; +} + +.list-name, +.button-list-edit { margin: 0; padding: 1em; - display: flex; - flex: 1 0; color: $fallback--link; color: var(--link, $fallback--link); @@ -30,4 +44,8 @@ --lightText: var(--selectedMenuLightText, $fallback--lightText); } } + +.list-name { + flex-grow: 1; +} diff --git a/src/components/list_edit/list_edit.js b/src/components/list_edit/list_edit.js new file mode 100644 index 00000000..0e03dbcb --- /dev/null +++ b/src/components/list_edit/list_edit.js @@ -0,0 +1,106 @@ +import { mapState, mapGetters } from 'vuex' +import BasicUserCard from '../basic_user_card/basic_user_card.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 ListNew = { + components: { + BasicUserCard, + UserAvatar + }, + data () { + return { + title: '', + suggestions: [], + userIds: [], + selectedUserIds: [], + loading: false, + query: '' + } + }, + created () { + this.$store.state.api.backendInteractor.getList({ id: this.id }) + .then((data) => { this.title = data.title }) + this.$store.state.api.backendInteractor.getListAccounts({ id: this.id }) + .then((data) => { this.selectedUserIds = data }) + }, + computed: { + id () { + return this.$route.params.id + }, + users () { + return this.userIds.map(userId => this.findUser(userId)) + }, + availableUsers () { + if (this.query.length !== 0) { + return this.users + } else { + return this.suggestions + } + }, + ...mapState({ + currentUser: state => state.users.currentUser, + backendInteractor: state => state.api.backendInteractor + }), + ...mapGetters(['findUser']) + }, + methods: { + onInput () { + this.search(this.query) + }, + selectUser (user, event) { + if (this.selectedUserIds.includes(user.id)) { + this.removeUser(user.id) + event.target.classList.remove('selected') + } else { + this.addUser(user) + event.target.classList.add('selected') + } + }, + addUser (user) { + this.selectedUserIds.push(user.id) + }, + removeUser (userId) { + this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId) + }, + search (query) { + if (!query) { + this.loading = false + return + } + + this.loading = true + this.userIds = [] + this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts', following: true }) + .then(data => { + this.loading = false + this.userIds = data.accounts.map(a => a.id) + }) + }, + updateList () { + // the API has two different endpoints for "updating the list name" and + // "updating the accounts on the list". + this.$store.state.api.backendInteractor.updateList({ id: this.id, title: this.title }) + this.$store.state.api.backendInteractor.addAccountsToList({ + id: this.id, accountIds: this.selectedUserIds + }).then(() => { + this.$router.push({ name: 'list-timeline', params: { id: this.id } }) + }) + }, + deleteList () { + this.$store.state.api.backendInteractor.deleteList({ id: this.id }) + .then(this.$router.push({ name: 'lists' })) + } + } +} + +export default ListNew diff --git a/src/components/list_edit/list_edit.vue b/src/components/list_edit/list_edit.vue new file mode 100644 index 00000000..cadd25da --- /dev/null +++ b/src/components/list_edit/list_edit.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/src/i18n/en.json b/src/i18n/en.json index e113400f..3430620b 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -952,7 +952,9 @@ "new": "New List", "title": "List title", "search": "Search users", - "create": "Create" + "create": "Create", + "save": "Save changes", + "delete": "Delete list" }, "file_type": { "audio": "Audio", diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index ff6c00d5..7f2fc5ac 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -50,6 +50,7 @@ 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}` @@ -403,6 +404,31 @@ const createList = ({ title, credentials }) => { }).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, { + method: 'PUT', + headers: headers, + 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) @@ -415,6 +441,14 @@ const addAccountsToList = ({ id, accountIds, credentials }) => { }) } +const deleteList = ({ id, credentials }) => { + const url = MASTODON_LIST_URL(id) + return fetch(url, { + method: 'DELETE', + headers: authHeaders(credentials) + }) +} + const fetchConversation = ({ id, credentials }) => { let urlContext = MASTODON_STATUS_CONTEXT_URL(id) return fetch(urlContext, { headers: authHeaders(credentials) }) @@ -1389,7 +1423,11 @@ const apiService = { fetchFollowRequests, fetchLists, createList, + getList, + updateList, + getListAccounts, addAccountsToList, + deleteList, approveUser, denyUser, suggestions,