diff options
Diffstat (limited to 'src/components')
21 files changed, 819 insertions, 6 deletions
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 @@ +<template> + <div v-if="isNew"> + <ListsNew @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"> + <ListsCard + v-for="list in lists.slice().reverse()" + :key="list" + :list="list" + class="list-item" + /> + </div> + </div> +</template> + +<script src="./lists.js"></script> 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..13866d8c --- /dev/null +++ b/src/components/lists_card/lists_card.vue @@ -0,0 +1,51 @@ +<template> + <div class="list-card"> + <router-link + :to="{ name: 'lists-timeline', params: { id: list.id } }" + class="list-name" + > + {{ list.title }} + </router-link> + <router-link + :to="{ name: 'lists-edit', params: { id: list.id } }" + class="button-list-edit" + > + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="ellipsis-h" + /> + </router-link> + </div> +</template> + +<script src="./lists_card.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.list-card { + display: flex; +} + +.list-name, +.button-list-edit { + margin: 0; + padding: 1em; + color: $fallback--link; + color: var(--link, $fallback--link); + + &:hover { + background-color: $fallback--lightBg; + background-color: var(--selectedMenu, $fallback--lightBg); + color: $fallback--link; + color: var(--selectedMenuText, $fallback--link); + --faint: var(--selectedMenuFaintText, $fallback--faint); + --faintLink: var(--selectedMenuFaintLink, $fallback--faint); + --lightText: var(--selectedMenuLightText, $fallback--lightText); + } +} + +.list-name { + flex-grow: 1; +} +</style> diff --git a/src/components/lists_edit/lists_edit.js b/src/components/lists_edit/lists_edit.js new file mode 100644 index 00000000..a68bb589 --- /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: 'lists-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 @@ +<template> + <div class="panel-default panel list-edit"> + <div + ref="header" + class="panel-heading" + > + <button + class="button-unstyled go-back-button" + @click="$router.back" + > + <FAIcon + size="lg" + icon="chevron-left" + /> + </button> + </div> + <div class="input-wrap"> + <input + ref="title" + v-model="title" + :placeholder="$t('lists.title')" + > + </div> + <div class="member-list"> + <div + v-for="user in selectedUsers" + :key="user.id" + class="member" + > + <BasicUserCard + :user="user" + :class="isSelected(user) ? 'selected' : ''" + @click.capture.prevent="selectUser(user)" + /> + </div> + </div> + <ListsUserSearch @results="onResults" /> + <div class="member-list"> + <div + v-for="user in users" + :key="user.id" + class="member" + > + <BasicUserCard + :user="user" + :class="isSelected(user) ? 'selected' : ''" + @click.capture.prevent="selectUser(user)" + /> + </div> + </div> + <button + :disabled="title && title.length === 0" + class="btn button-default" + @click="updateList" + > + {{ $t('lists.save') }} + </button> + <button + class="btn button-default" + @click="deleteList" + > + {{ $t('lists.delete') }} + </button> + </div> +</template> + +<script src="./lists_edit.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.list-edit { + .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_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..e910d6eb --- /dev/null +++ b/src/components/lists_menu/lists_menu_content.vue @@ -0,0 +1,17 @@ +<template> + <ul> + <li + v-for="list in lists.slice().reverse()" + :key="list.id" + > + <router-link + class="menu-item" + :to="{ name: 'lists-timeline', params: { id: list.id } }" + > + {{ list.title }} + </router-link> + </li> + </ul> +</template> + +<script src="./lists_menu_content.js"></script> diff --git a/src/components/lists_new/lists_new.js b/src/components/lists_new/lists_new.js new file mode 100644 index 00000000..63dc28ad --- /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: 'lists-timeline', params: { id: list.id } }) + }) + } + } +} + +export default ListsNew diff --git a/src/components/lists_new/lists_new.vue b/src/components/lists_new/lists_new.vue new file mode 100644 index 00000000..4733bdde --- /dev/null +++ b/src/components/lists_new/lists_new.vue @@ -0,0 +1,95 @@ +<template> + <div class="panel-default panel list-new"> + <div + ref="header" + class="panel-heading" + > + <button + class="button-unstyled go-back-button" + @click="goBack" + > + <FAIcon + size="lg" + icon="chevron-left" + /> + </button> + </div> + <div class="input-wrap"> + <input + ref="title" + v-model="title" + :placeholder="$t('lists.title')" + > + </div> + + <div class="member-list"> + <div + v-for="user in selectedUsers" + :key="user.id" + class="member" + > + <BasicUserCard + :user="user" + :class="isSelected(user) ? 'selected' : ''" + @click.capture.prevent="selectUser(user)" + /> + </div> + </div> + <ListsUserSearch + @results="onResults" + /> + <div + v-for="user in users" + :key="user.id" + class="member" + > + <BasicUserCard + :user="user" + :class="isSelected(user) ? 'selected' : ''" + @click.capture.prevent="selectUser(user)" + /> + </div> + + <button + :disabled="title && title.length === 0" + class="btn button-default" + @click="createList" + > + {{ $t('lists.create') }} + </button> + </div> +</template> + +<script src="./lists_new.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.list-new { + .search-icon { + margin-right: 0.3em; + } + + .member-list { + padding-bottom: 0.7rem; + } + + .basic-user-card:hover, + .basic-user-card.selected { + cursor: pointer; + background-color: var(--selectedPost, $fallback--lightBg); + } + + .go-back-button { + text-align: center; + line-height: 1; + height: 100%; + align-self: start; + width: var(--__panel-heading-height-inner); + } + + .btn { + margin: 0.5em; + } +} +</style> diff --git a/src/components/lists_timeline/lists_timeline.js b/src/components/lists_timeline/lists_timeline.js new file mode 100644 index 00000000..7534420d --- /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 === 'lists-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 @@ +<template> + <Timeline + title="list.name" + :timeline="timeline" + :list-id="listId" + timeline-name="list" + /> +</template> + +<script src="./lists_timeline.js"></script> 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 @@ +<template> + <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="input-wrap"> + <Checkbox + v-model="followingOnly" + @change="onInput" + > + {{ $t('lists.following_only') }} + </Checkbox> + </div> + </div> +</template> + +<script src="./lists_user_search.js"></script> +<style lang="scss"> +@import '../../_variables.scss'; + +.input-wrap { + display: flex; + margin: 0.7em 0.5em 0.7em 0.5em; + + input { + width: 100%; + } +} + +.search-icon { + margin-right: 0.3em; +} + +</style> 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 @@ <TimelineMenuContent class="timelines" /> </div> </li> + <li v-if="currentUser && listsNavigation"> + <button + class="button-unstyled menu-item" + @click="toggleLists" + > + <router-link + :to="{ name: 'lists' }" + @click.stop + > + <FAIcon + fixed-width + class="fa-scale-110" + icon="list" + />{{ $t("nav.lists") }} + </router-link> + <FAIcon + class="timelines-chevron" + fixed-width + :icon="showLists ? 'chevron-up' : 'chevron-down'" + /> + </button> + <div + v-show="showLists" + class="timelines-background" + > + <ListsMenuContent class="timelines" /> + </div> + </li> + <li v-if="currentUser && !listsNavigation"> + <router-link + :to="{ name: 'lists' }" + @click.stop + > + <button + class="button-unstyled menu-item" + @click="toggleLists" + > + <FAIcon + fixed-width + class="fa-scale-110" + icon="list" + />{{ $t("nav.lists") }} + </button> + </router-link> + </li> <li v-if="currentUser"> <router-link class="menu-item" diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index a2609200..91015955 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -124,6 +124,53 @@ {{ $t('settings.hide_shoutbox') }} </BooleanSetting> </li> + <li> + <BooleanSetting path="listsNavigation"> + {{ $t('settings.lists_navigation') }} + </BooleanSetting> + </li> + <li> + <h3>{{ $t('settings.columns') }}</h3> + </li> + <li> + <BooleanSetting path="disableStickyHeaders"> + {{ $t('settings.disable_sticky_headers') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="showScrollbars"> + {{ $t('settings.show_scrollbars') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="sidebarRight"> + {{ $t('settings.right_sidebar') }} + </BooleanSetting> + </li> + <li> + <ChoiceSetting + v-if="user" + id="thirdColumnMode" + path="thirdColumnMode" + :options="thirdColumnModeOptions" + > + {{ $t('settings.third_column_mode') }} + </ChoiceSetting> + </li> + <li v-if="expertLevel > 0"> + {{ $t('settings.column_sizes') }} + <div class="column-settings"> + <SizeSetting + v-for="column in columns" + :key="column" + :path="column + 'ColumnWidth'" + :units="horizontalUnits" + expert="1" + > + {{ $t('settings.column_sizes_' + column) }} + </SizeSetting> + </div> + </li> </ul> </div> <div class="setting-item"> 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 @@ -56,6 +56,18 @@ </router-link> </li> <li + v-if="currentUser" + @click="toggleDrawer" + > + <router-link :to="{ name: 'lists' }"> + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="list" + /> {{ $t("nav.lists") }} + </router-link> + </li> + <li v-if="currentUser && pleromaChatMessagesAvailable" @click="toggleDrawer" > diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index f195940a..8f6cae66 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -19,6 +19,7 @@ const Timeline = { 'timelineName', 'title', 'userId', + 'listId', 'tag', 'embedded', 'count', @@ -103,6 +104,7 @@ const Timeline = { timeline: this.timelineName, showImmediately, userId: this.userId, + listId: this.listId, tag: this.tag }) }, @@ -158,6 +160,7 @@ const Timeline = { older: true, showImmediately: true, userId: this.userId, + listId: this.listId, tag: this.tag }).then(({ statuses }) => { 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..5a67b0a5 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 === 'lists-timeline') { + return this.$store.getters.findListTitle(this.$route.params.id) + } const i18nkey = timelineNames()[this.$route.name] return i18nkey ? this.$t(i18nkey) : route } |
