From bacb6c8fb38310a44e583e87b621cec4b61a7a79 Mon Sep 17 00:00:00 2001 From: Sol Fisher Romanoff <sol@solfisher.com> Date: Thu, 16 Jun 2022 01:52:24 +0300 Subject: [PATCH] Add list creation functionality --- src/components/list_new/list_new.js | 97 ++++++++++++++++++++++++++ src/components/list_new/list_new.vue | 100 +++++++++++++++++++++++++++ src/components/lists/lists.js | 17 ++++- src/components/lists/lists.vue | 14 +++- src/i18n/en.json | 6 +- src/services/api/api.service.js | 27 ++++++++ 6 files changed, 258 insertions(+), 3 deletions(-) create mode 100644 src/components/list_new/list_new.js create mode 100644 src/components/list_new/list_new.vue diff --git a/src/components/list_new/list_new.js b/src/components/list_new/list_new.js new file mode 100644 index 00000000..7ac7e0f0 --- /dev/null +++ b/src/components/list_new/list_new.js @@ -0,0 +1,97 @@ +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: '' + } + }, + computed: { + 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: { + goBack () { + this.$emit('cancel') + }, + 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) + }) + }, + createList () { + // the API has two different endpoints for "creating a list with a name" + // and "updating the accounts on the list". + this.$store.state.api.backendInteractor.createList({ title: this.title }) + .then((data) => { + this.$store.state.api.backendInteractor.addAccountsToList({ + id: data.id, accountIds: this.selectedUserIds + }) + this.$router.push({ name: 'list-timeline', params: { id: data.id } }) + }) + } + } +} + +export default ListNew diff --git a/src/components/list_new/list_new.vue b/src/components/list_new/list_new.vue new file mode 100644 index 00000000..952f7732 --- /dev/null +++ b/src/components/list_new/list_new.vue @@ -0,0 +1,100 @@ +<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="input-wrap"> + <div class="input-search"> + <FAIcon + class="search-icon fa-scale-110 fa-old-padding" + icon="search" + /> + </div> + <input + ref="search" + v-model="query" + :placeholder="$t('lists.search')" + @input="onInput" + > + </div> + <div class="member-list"> + <div + v-for="user in availableUsers" + :key="user.id" + class="member" + > + <div @click.capture.prevent="selectUser(user, $event)"> + <BasicUserCard :user="user" /> + </div> + </div> + </div> + <button + :disabled="title && title.length === 0" + class="btn button-default" + @click="createList" + > + {{ $t('lists.create') }} + </button> + </div> +</template> + +<script src="./list_new.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.list-new { + .input-wrap { + display: flex; + margin: 0.7em 0.5em 0.7em 0.5em; + + input { + width: 100%; + } + } + + .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/lists.js b/src/components/lists/lists.js index bb67fafb..f511f0f5 100644 --- a/src/components/lists/lists.js +++ b/src/components/lists/lists.js @@ -1,8 +1,15 @@ import ListCard from '../list_card/list_card.vue' +import ListNew from '../list_new/list_new.vue' const Lists = { + data () { + return { + isNew: false + } + }, components: { - ListCard + ListCard, + ListNew }, created () { this.$store.dispatch('startFetchingLists') @@ -11,6 +18,14 @@ const Lists = { lists () { return this.$store.state.api.lists } + }, + methods: { + cancelNewList () { + this.isNew = false + }, + newList () { + this.isNew = true + } } } diff --git a/src/components/lists/lists.vue b/src/components/lists/lists.vue index 20dd46d8..f11a2a02 100644 --- a/src/components/lists/lists.vue +++ b/src/components/lists/lists.vue @@ -1,9 +1,21 @@ <template> - <div class="settings panel panel-default"> + <div v-if="isNew"> + <ListNew @cancel="cancelNewList" /> + </div> + <div + v-else + class="settings panel panel-default" + > <div class="panel-heading"> <div class="title"> {{ $t('lists.lists') }} </div> + <button + class="button-default" + @click="newList" + > + {{ $t("lists.new") }} + </button> </div> <div class="panel-body"> <ListCard diff --git a/src/i18n/en.json b/src/i18n/en.json index a80e0077..e113400f 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -948,7 +948,11 @@ "empty_chat_list_placeholder": "You don't have any chats yet. Start a new chat!" }, "lists": { - "lists": "Lists" + "lists": "Lists", + "new": "New List", + "title": "List title", + "search": "Search users", + "create": "Create" }, "file_type": { "audio": "Audio", diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 7c7e657d..ff6c00d5 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -51,6 +51,7 @@ 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_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/' @@ -390,6 +391,30 @@ const fetchLists = ({ 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, { + method: 'POST', + headers: headers, + body: JSON.stringify({ title }) + }).then((data) => data.json()) +} + +const addAccountsToList = ({ id, accountIds, credentials }) => { + const url = MASTODON_LIST_ACCOUNTS_URL(id) + const headers = authHeaders(credentials) + headers['Content-Type'] = 'application/json' + + return fetch(url, { + method: 'POST', + headers: headers, + body: JSON.stringify({ account_ids: accountIds }) + }) +} + const fetchConversation = ({ id, credentials }) => { let urlContext = MASTODON_STATUS_CONTEXT_URL(id) return fetch(urlContext, { headers: authHeaders(credentials) }) @@ -1363,6 +1388,8 @@ const apiService = { mfaConfirmOTP, fetchFollowRequests, fetchLists, + createList, + addAccountsToList, approveUser, denyUser, suggestions,