Merge pull request #911 from h3poteto/iss-772

closes #772 Add a menu to read follow requests, and accept/reject it
This commit is contained in:
AkiraFukushima 2019-05-14 23:34:43 +09:00 committed by GitHub
commit 5bfd369646
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 416 additions and 159 deletions

View File

@ -26,6 +26,7 @@ const state = (): SideMenuState => {
unreadLocalTimeline: false, unreadLocalTimeline: false,
unreadDirectMessagesTimeline: false, unreadDirectMessagesTimeline: false,
unreadPublicTimeline: false, unreadPublicTimeline: false,
unreadFollowRequests: false,
lists: [], lists: [],
tags: [], tags: [],
collapse: false collapse: false
@ -62,10 +63,7 @@ describe('SideMenu', () => {
get: (_path: string, _params: object) => { get: (_path: string, _params: object) => {
return new Promise<Response<List[]>>(resolve => { return new Promise<Response<List[]>>(resolve => {
const res: Response<List[]> = { const res: Response<List[]> = {
data: [ data: [list1, list2],
list1,
list2
],
status: 200, status: 200,
statusText: 'OK', statusText: 'OK',
headers: {} headers: {}

View File

@ -1,10 +1,13 @@
{ {
"side_menu": { "side_menu": {
"direct": "Direct messages" "direct": "Direct messages",
"follow_requests": "Follow Requests"
}, },
"header_menu": { "header_menu": {
"settings": "Settings", "settings": "Settings",
"switch_streaming": "Use websocket for streaming. If the timeline does not update with streaming, please try it." "switch_streaming": "Use websocket for streaming. If the timeline does not update with streaming, please try it.",
"follow_requests": "Follow Requests",
"direct_messages": "Direct Messages"
}, },
"settings": { "settings": {
"timeline": { "timeline": {
@ -38,5 +41,13 @@
"hideAllAttachments": "Hide all medias" "hideAllAttachments": "Hide all medias"
} }
} }
},
"follow_requests": {
"accept": "Accept",
"reject": "Reject"
},
"message": {
"follow_request_accept_error": "Failed to accept the request",
"follow_reuqest_reject_error": "failed to reject the request"
} }
} }

View File

@ -49,6 +49,7 @@
"notification": "Notification", "notification": "Notification",
"mention": "Mention", "mention": "Mention",
"direct": "Direct messages", "direct": "Direct messages",
"follow_requests": "Follow Requests",
"favourite": "Favourite", "favourite": "Favourite",
"local": "Local timeline", "local": "Local timeline",
"public": "Public timeline", "public": "Public timeline",
@ -61,6 +62,8 @@
"notification": "Notification", "notification": "Notification",
"mention": "Mention", "mention": "Mention",
"favourite": "Favourite", "favourite": "Favourite",
"follow_requests": "Follow Requests",
"direct_messages": "Direct Messages",
"local": "Local timeline", "local": "Local timeline",
"public": "Public timeline", "public": "Public timeline",
"hashtag": "Hashtag", "hashtag": "Hashtag",
@ -298,6 +301,10 @@
"followers": "Followers" "followers": "Followers"
} }
}, },
"follow_requests": {
"accept": "Accept",
"reject": "Reject"
},
"hashtag": { "hashtag": {
"tag_name": "Tag name", "tag_name": "Tag name",
"delete_tag": "Delete tag", "delete_tag": "Delete tag",
@ -337,6 +344,8 @@
"timeline_fetch_error": "Failed to fetch timeline", "timeline_fetch_error": "Failed to fetch timeline",
"notification_fetch_error": "Failed to fetch notification", "notification_fetch_error": "Failed to fetch notification",
"favourite_fetch_error": "Failed to fetch favorite", "favourite_fetch_error": "Failed to fetch favorite",
"follow_request_accept_error": "Failed to accept the request",
"follow_reuqest_reject_error": "failed to reject the request",
"start_streaming_error": "Failed to start streaming", "start_streaming_error": "Failed to start streaming",
"attach_error": "Could not attach the file", "attach_error": "Could not attach the file",
"authorize_duplicate_error": "Can not login the same account of the same domain", "authorize_duplicate_error": "Can not login the same account of the same domain",

View File

@ -0,0 +1,17 @@
{
"side_menu": {
"follow_requests": "Follow Requests"
},
"header_menu": {
"follow_requests": "Follow Requests",
"direct_messages": "Direct Messages"
},
"follow_requests": {
"accept": "Accept",
"reject": "Reject"
},
"message": {
"follow_request_accept_error": "Failed to accept the request",
"follow_reuqest_reject_error": "failed to reject the request"
}
}

View File

@ -1 +1,17 @@
{
"side_menu": {
"follow_requests": "Follow Requests"
},
"header_menu": {
"follow_requests": "Follow Requests",
"direct_messages": "Direct Messages"
},
"follow_requests": {
"accept": "Accept",
"reject": "Reject"
},
"message": {
"follow_request_accept_error": "Failed to accept the request",
"follow_reuqest_reject_error": "failed to reject the request"
}
}

View File

@ -48,6 +48,7 @@
"home": "ホーム", "home": "ホーム",
"notification": "通知", "notification": "通知",
"direct": "ダイレクトメッセージ", "direct": "ダイレクトメッセージ",
"follow_requests": "フォローリクエスト",
"favourite": "お気に入り", "favourite": "お気に入り",
"local": "ローカル", "local": "ローカル",
"public": "連合", "public": "連合",
@ -59,6 +60,8 @@
"home": "ホーム", "home": "ホーム",
"notification": "通知", "notification": "通知",
"favourite": "お気に入り", "favourite": "お気に入り",
"follow_requests": "フォローリクエスト",
"direct_messages": "ダイレクトメッセージ",
"local": "ローカルタイムライン", "local": "ローカルタイムライン",
"public": "連合タイムライン", "public": "連合タイムライン",
"hashtag": "ハッシュタグ", "hashtag": "ハッシュタグ",
@ -289,6 +292,10 @@
"followers": "フォロワー" "followers": "フォロワー"
} }
}, },
"follow_requests": {
"accept": "承認",
"reject": "却下"
},
"hashtag": { "hashtag": {
"tag_name": "タグ名", "tag_name": "タグ名",
"delete_tag": "タグを削除", "delete_tag": "タグを削除",
@ -328,6 +335,8 @@
"timeline_fetch_error": "タイムラインの読み込みに失敗しました", "timeline_fetch_error": "タイムラインの読み込みに失敗しました",
"notification_fetch_error": "通知の読み込みに失敗しました", "notification_fetch_error": "通知の読み込みに失敗しました",
"favourite_fetch_error": "お気に入りの読み込みに失敗しました", "favourite_fetch_error": "お気に入りの読み込みに失敗しました",
"follow_request_accept_error": "フォローリクエストの承認に失敗しました",
"follow_reuqest_reject_error": "フォローリクエストの却下に失敗しました",
"start_streaming_error": "ストリーミングを開始できませんでした", "start_streaming_error": "ストリーミングを開始できませんでした",
"attach_error": "ファイルを添付できませんでした", "attach_error": "ファイルを添付できませんでした",
"authorize_duplicate_error": "同一ドメイン同一アカウントではログインできません", "authorize_duplicate_error": "同一ドメイン同一アカウントではログインできません",

View File

@ -9,5 +9,20 @@
"hideAllAttachments": "Hide all medias" "hideAllAttachments": "Hide all medias"
} }
} }
},
"side_menu": {
"follow_requests": "Follow Requests"
},
"header_menu": {
"follow_requests": "Follow Requests",
"direct_messages": "Direct Messages"
},
"follow_requests": {
"accept": "Accept",
"reject": "Reject"
},
"message": {
"follow_request_accept_error": "Failed to accept the request",
"follow_reuqest_reject_error": "failed to reject the request"
} }
} }

View File

@ -1,10 +1,13 @@
{ {
"side_menu": { "side_menu": {
"direct": "Direct messages" "direct": "Direct messages",
"follow_requests": "Follow Requests"
}, },
"header_menu": { "header_menu": {
"settings": "Settings", "settings": "Settings",
"switch_streaming": "Use websocket for streaming. If the timeline does not update with streaming, please try it." "switch_streaming": "Use websocket for streaming. If the timeline does not update with streaming, please try it.",
"follow_requests": "Follow Requests",
"direct_messages": "Direct Messages"
}, },
"settings": { "settings": {
"timeline": { "timeline": {
@ -38,5 +41,13 @@
"hideAllAttachments": "Hide all medias" "hideAllAttachments": "Hide all medias"
} }
} }
},
"follow_requests": {
"accept": "Accept",
"reject": "Reject"
},
"message": {
"follow_request_accept_error": "Failed to accept the request",
"follow_reuqest_reject_error": "failed to reject the request"
} }
} }

View File

@ -1,15 +1,15 @@
<template> <template>
<div <div
id="timeline_space" id="timeline_space"
v-loading="loading" v-loading="loading"
:element-loading-text="$t('message.loading')" :element-loading-text="$t('message.loading')"
element-loading-spinner="el-icon-loading" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.8)" element-loading-background="rgba(0, 0, 0, 0.8)"
v-shortkey="shortcutEnabled ? {help: ['shift', '?']} : {}" v-shortkey="shortcutEnabled ? { help: ['shift', '?'] } : {}"
@shortkey="handleKey" @shortkey="handleKey"
> >
<side-menu></side-menu> <side-menu></side-menu>
<div :class="collapse ? 'page-narrow':'page'"> <div :class="collapse ? 'page-narrow' : 'page'">
<header class="header" style="-webkit-app-region: drag;"> <header class="header" style="-webkit-app-region: drag;">
<header-menu></header-menu> <header-menu></header-menu>
</header> </header>
@ -17,7 +17,7 @@
</div> </div>
<modals></modals> <modals></modals>
<receive-drop v-show="droppableVisible"></receive-drop> <receive-drop v-show="droppableVisible"></receive-drop>
</div> </div>
</template> </template>
<script> <script>
@ -32,7 +32,7 @@ import ReceiveDrop from './TimelineSpace/ReceiveDrop'
export default { export default {
name: 'timeline-space', name: 'timeline-space',
components: { SideMenu, HeaderMenu, Modals, Contents, ReceiveDrop }, components: { SideMenu, HeaderMenu, Modals, Contents, ReceiveDrop },
data () { data() {
return { return {
dropTarget: null, dropTarget: null,
droppableVisible: false droppableVisible: false
@ -43,23 +43,20 @@ export default {
loading: state => state.TimelineSpace.loading, loading: state => state.TimelineSpace.loading,
collapse: state => state.TimelineSpace.SideMenu.collapse collapse: state => state.TimelineSpace.SideMenu.collapse
}), }),
...mapGetters('TimelineSpace/Modals', [ ...mapGetters('TimelineSpace/Modals', ['modalOpened']),
'modalOpened' shortcutEnabled: function() {
]),
shortcutEnabled: function () {
return !this.modalOpened return !this.modalOpened
} }
}, },
async created () { async created() {
this.$store.dispatch('TimelineSpace/Contents/SideBar/close') this.$store.dispatch('TimelineSpace/Contents/SideBar/close')
this.$store.commit('TimelineSpace/changeLoading', true) this.$store.commit('TimelineSpace/changeLoading', true)
await this.initialize() await this.initialize().finally(() => {
.finally(() => {
this.$store.commit('TimelineSpace/changeLoading', false) this.$store.commit('TimelineSpace/changeLoading', false)
this.$store.commit('GlobalHeader/updateChanging', false) this.$store.commit('GlobalHeader/updateChanging', false)
}) })
}, },
mounted () { mounted() {
window.addEventListener('dragenter', this.onDragEnter) window.addEventListener('dragenter', this.onDragEnter)
window.addEventListener('dragleave', this.onDragLeave) window.addEventListener('dragleave', this.onDragLeave)
window.addEventListener('dragover', this.onDragOver) window.addEventListener('dragover', this.onDragOver)
@ -68,7 +65,7 @@ export default {
this.$store.commit('TimelineSpace/Modals/Jump/changeModal', true) this.$store.commit('TimelineSpace/Modals/Jump/changeModal', true)
}) })
}, },
beforeDestroy () { beforeDestroy() {
window.removeEventListener('dragenter', this.onDragEnter) window.removeEventListener('dragenter', this.onDragEnter)
window.removeEventListener('dragleave', this.onDragLeave) window.removeEventListener('dragleave', this.onDragLeave)
window.removeEventListener('dragover', this.onDragOver) window.removeEventListener('dragover', this.onDragOver)
@ -77,14 +74,14 @@ export default {
this.$store.dispatch('TimelineSpace/unbindStreamings') this.$store.dispatch('TimelineSpace/unbindStreamings')
}, },
methods: { methods: {
async clear () { async clear() {
await this.$store.dispatch('TimelineSpace/clearAccount') await this.$store.dispatch('TimelineSpace/clearAccount')
this.$store.dispatch('TimelineSpace/clearContentsTimelines') this.$store.dispatch('TimelineSpace/clearContentsTimelines')
await this.$store.dispatch('TimelineSpace/removeShortcutEvents') await this.$store.dispatch('TimelineSpace/removeShortcutEvents')
await this.$store.dispatch('TimelineSpace/clearUnread') await this.$store.dispatch('TimelineSpace/clearUnread')
return 'clear' return 'clear'
}, },
async initialize () { async initialize() {
await this.clear() await this.clear()
this.$store.dispatch('TimelineSpace/watchShortcutEvents') this.$store.dispatch('TimelineSpace/watchShortcutEvents')
@ -95,11 +92,11 @@ export default {
}) })
}) })
this.$store.dispatch('TimelineSpace/SideMenu/fetchLists', account) this.$store.dispatch('TimelineSpace/SideMenu/fetchLists', account)
this.$store.dispatch('TimelineSpace/SideMenu/fetchFollowRequests', account)
await this.$store.dispatch('TimelineSpace/loadUnreadNotification', this.$route.params.id) await this.$store.dispatch('TimelineSpace/loadUnreadNotification', this.$route.params.id)
// Load timelines // Load timelines
await this.$store.dispatch('TimelineSpace/fetchContentsTimelines', account) await this.$store.dispatch('TimelineSpace/fetchContentsTimelines', account).catch(_ => {
.catch(_ => {
this.$message({ this.$message({
message: this.$t('message.timeline_fetch_error'), message: this.$t('message.timeline_fetch_error'),
type: 'error' type: 'error'
@ -115,7 +112,7 @@ export default {
this.$store.dispatch('TimelineSpace/fetchEmojis', account) this.$store.dispatch('TimelineSpace/fetchEmojis', account)
this.$store.dispatch('TimelineSpace/fetchInstance', account) this.$store.dispatch('TimelineSpace/fetchInstance', account)
}, },
handleDrop (e) { handleDrop(e) {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
this.droppableVisible = false this.droppableVisible = false
@ -132,8 +129,7 @@ export default {
} }
this.$store.dispatch('TimelineSpace/Modals/NewToot/openModal') this.$store.dispatch('TimelineSpace/Modals/NewToot/openModal')
this.$store.dispatch('TimelineSpace/Modals/NewToot/incrementMediaId') this.$store.dispatch('TimelineSpace/Modals/NewToot/incrementMediaId')
this.$store.dispatch('TimelineSpace/Modals/NewToot/uploadImage', file) this.$store.dispatch('TimelineSpace/Modals/NewToot/uploadImage', file).catch(() => {
.catch(() => {
this.$message({ this.$message({
message: this.$t('message.attach_error'), message: this.$t('message.attach_error'),
type: 'error' type: 'error'
@ -141,19 +137,19 @@ export default {
}) })
return false return false
}, },
onDragEnter (e) { onDragEnter(e) {
this.dropTarget = e.target this.dropTarget = e.target
this.droppableVisible = true this.droppableVisible = true
}, },
onDragLeave (e) { onDragLeave(e) {
if (e.target === this.dropTarget) { if (e.target === this.dropTarget) {
this.droppableVisible = false this.droppableVisible = false
} }
}, },
onDragOver (e) { onDragOver(e) {
e.preventDefault() e.preventDefault()
}, },
handleKey (event) { handleKey(event) {
switch (event.srcKey) { switch (event.srcKey) {
case 'help': case 'help':
this.$store.commit('TimelineSpace/Modals/Shortcut/changeModal', true) this.$store.commit('TimelineSpace/Modals/Shortcut/changeModal', true)
@ -205,5 +201,4 @@ export default {
width: calc(100% - 65px - 64px); width: calc(100% - 65px - 64px);
} }
} }
</style> </style>

View File

@ -0,0 +1,53 @@
<template>
<div id="follow-requests">
<template v-for="account in requests">
<user :user="account" :request="true" @acceptRequest="accept" @rejectRequest="reject"></user>
</template>
</div>
</template>
<script>
import { mapState } from 'vuex'
import User from '@/components/molecules/User'
export default {
name: 'folllow-requests',
components: { User },
computed: {
...mapState('TimelineSpace/Contents/FollowRequests', {
requests: state => state.requests
})
},
async mounted() {
await this.initialize()
},
methods: {
async initialize() {
await this.$store.dispatch('TimelineSpace/Contents/FollowRequests/fetchRequests').catch(_ => {
this.$message({
message: this.$t('message.timeline_fetch_error'),
type: 'error'
})
})
},
accept(account) {
this.$store.dispatch('TimelineSpace/Contents/FollowRequests/acceptRequest', account).catch(_ => {
this.$message({
message: this.$t('message.follow_request_accept_error'),
type: 'error'
})
})
},
reject(account) {
this.$store.dispatch('TimelineSpace/Contents/FollowRequests/rejectRequest', account).catch(_ => {
this.$message({
message: this.$t('message.follow_request_reject_error'),
type: 'error'
})
})
}
}
}
</script>
<style lang="scss" scorped></style>

View File

@ -115,6 +115,9 @@ export default {
case 'mentions': case 'mentions':
this.$store.commit('TimelineSpace/HeaderMenu/updateTitle', this.$t('header_menu.mention')) this.$store.commit('TimelineSpace/HeaderMenu/updateTitle', this.$t('header_menu.mention'))
break break
case 'follow-requests':
this.$store.commit('TimelineSpace/HeaderMenu/updateTitle', this.$t('header_menu.follow_requests'))
break
case 'local': case 'local':
this.$store.commit('TimelineSpace/HeaderMenu/updateTitle', this.$t('header_menu.local')) this.$store.commit('TimelineSpace/HeaderMenu/updateTitle', this.$t('header_menu.local'))
break break
@ -134,7 +137,7 @@ export default {
this.$store.commit('TimelineSpace/HeaderMenu/updateTitle', this.$t('header_menu.lists')) this.$store.commit('TimelineSpace/HeaderMenu/updateTitle', this.$t('header_menu.lists'))
break break
case 'direct-messages': case 'direct-messages':
this.$store.commit('TimelineSpace/HeaderMenu/updateTitle', 'Direct Messages') this.$store.commit('TimelineSpace/HeaderMenu/updateTitle', this.$t('header_menu.direct_messages'))
break break
case 'edit-list': case 'edit-list':
this.$store.commit('TimelineSpace/HeaderMenu/updateTitle', this.$t('header_menu.members')) this.$store.commit('TimelineSpace/HeaderMenu/updateTitle', this.$t('header_menu.members'))

View File

@ -1,6 +1,6 @@
<template> <template>
<div id="side_menu"> <div id="side_menu">
<div :class="collapse ? 'profile-wrapper narrow-menu':'profile-wrapper'" style="-webkit-app-region: drag;"> <div :class="collapse ? 'profile-wrapper narrow-menu' : 'profile-wrapper'" style="-webkit-app-region: drag;">
<div :class="collapse ? 'profile-narrow' : 'profile-wide'"> <div :class="collapse ? 'profile-narrow' : 'profile-wide'">
<div class="account"> <div class="account">
<div class="avatar" v-if="collapse"> <div class="avatar" v-if="collapse">
@ -15,9 +15,9 @@
<i class="el-icon-arrow-down el-icon--right"></i> <i class="el-icon-arrow-down el-icon--right"></i>
</span> </span>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item command="show">{{ $t("side_menu.show_profile") }}</el-dropdown-item> <el-dropdown-item command="show">{{ $t('side_menu.show_profile') }}</el-dropdown-item>
<el-dropdown-item command="edit">{{ $t("side_menu.edit_profile") }}</el-dropdown-item> <el-dropdown-item command="edit">{{ $t('side_menu.edit_profile') }}</el-dropdown-item>
<el-dropdown-item command="settings">{{ $t("side_menu.settings") }}</el-dropdown-item> <el-dropdown-item command="settings">{{ $t('side_menu.settings') }}</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</div> </div>
@ -38,68 +38,85 @@
:collapse="collapse" :collapse="collapse"
active-text-color="#ffffff" active-text-color="#ffffff"
:router="true" :router="true"
:class="collapse ? 'el-menu-vertical timeline-menu narrow-menu':'el-menu-vertical timeline-menu'" :class="collapse ? 'el-menu-vertical timeline-menu narrow-menu' : 'el-menu-vertical timeline-menu'"
role="menu"> role="menu"
>
<el-menu-item :index="`/${id()}/home`" role="menuitem" :title="$t('side_menu.home')"> <el-menu-item :index="`/${id()}/home`" role="menuitem" :title="$t('side_menu.home')">
<icon name="home"></icon> <icon name="home"></icon>
<span>{{ $t("side_menu.home") }}</span> <span>{{ $t('side_menu.home') }}</span>
<el-badge is-dot :hidden="!unreadHomeTimeline"> <el-badge is-dot :hidden="!unreadHomeTimeline"> </el-badge>
</el-badge>
</el-menu-item> </el-menu-item>
<el-menu-item :index="`/${id()}/notifications`" role="menuitem" :title="$t('side_menu.notification')"> <el-menu-item :index="`/${id()}/notifications`" role="menuitem" :title="$t('side_menu.notification')">
<icon name="bell"></icon> <icon name="bell"></icon>
<span>{{ $t("side_menu.notification") }}</span> <span>{{ $t('side_menu.notification') }}</span>
<el-badge is-dot :hidden="!unreadNotifications"> <el-badge is-dot :hidden="!unreadNotifications"> </el-badge>
</el-badge>
</el-menu-item> </el-menu-item>
<el-menu-item :index="`/${id()}/mentions`" role="menuitem" :title="$t('side_menu.mention')"> <el-menu-item :index="`/${id()}/mentions`" role="menuitem" :title="$t('side_menu.mention')">
<icon name="at"></icon> <icon name="at"></icon>
<span>{{ $t("side_menu.mention") }}</span> <span>{{ $t('side_menu.mention') }}</span>
<el-badge is-dot :hidden="!unreadMentions"> <el-badge is-dot :hidden="!unreadMentions"> </el-badge>
</el-badge>
</el-menu-item> </el-menu-item>
<el-menu-item :index="`/${id()}/direct-messages`" role="menuitem"> <el-menu-item :index="`/${id()}/direct-messages`" role="menuitem" :title="$t('side_menu.direct')">
<icon name="envelope"></icon> <icon name="envelope"></icon>
<span>{{ $t("side_menu.direct") }}</span> <span>{{ $t('side_menu.direct') }}</span>
<el-badge is-dot :hidden="!unreadDirectMessagesTimeline"> <el-badge is-dot :hidden="!unreadDirectMessagesTimeline"> </el-badge>
</el-badge> </el-menu-item>
<el-menu-item
v-if="unreadFollowRequests"
:index="`/${id()}/follow-requests`"
role="menuitem"
:title="$t('side_menu.follow_requests')"
>
<icon name="users"></icon>
<span>{{ $t('side_menu.follow_requests') }}</span>
<el-badge is-dot></el-badge>
</el-menu-item> </el-menu-item>
<el-menu-item :index="`/${id()}/favourites`" role="menuitem" :title="$t('side_menu.favourite')"> <el-menu-item :index="`/${id()}/favourites`" role="menuitem" :title="$t('side_menu.favourite')">
<icon name="star"></icon> <icon name="star"></icon>
<span>{{ $t("side_menu.favourite") }}</span> <span>{{ $t('side_menu.favourite') }}</span>
</el-menu-item> </el-menu-item>
<el-menu-item :index="`/${id()}/local`" role="menuitem" :title="$t('side_menu.local')"> <el-menu-item :index="`/${id()}/local`" role="menuitem" :title="$t('side_menu.local')">
<icon name="users"></icon> <icon name="users"></icon>
<span>{{ $t("side_menu.local") }}</span> <span>{{ $t('side_menu.local') }}</span>
<el-badge is-dot :hidden="!unreadLocalTimeline"> <el-badge is-dot :hidden="!unreadLocalTimeline"> </el-badge>
</el-badge>
</el-menu-item> </el-menu-item>
<el-menu-item :index="`/${id()}/public`" role="menuitem" :title="$t('side_menu.public')"> <el-menu-item :index="`/${id()}/public`" role="menuitem" :title="$t('side_menu.public')">
<icon name="globe"></icon> <icon name="globe"></icon>
<span>{{ $t("side_menu.public") }}</span> <span>{{ $t('side_menu.public') }}</span>
<el-badge is-dot :hidden="!unreadPublicTimeline"> <el-badge is-dot :hidden="!unreadPublicTimeline"> </el-badge>
</el-badge>
</el-menu-item> </el-menu-item>
<el-menu-item :index="`/${id()}/search`" role="menuitem" :title="$t('side_menu.search')"> <el-menu-item :index="`/${id()}/search`" role="menuitem" :title="$t('side_menu.search')">
<icon name="search"></icon> <icon name="search"></icon>
<span>{{ $t("side_menu.search") }}</span> <span>{{ $t('side_menu.search') }}</span>
</el-menu-item> </el-menu-item>
<el-menu-item :index="`/${id()}/hashtag`" role="menuitem" :title="$t('side_menu.hashtag')"> <el-menu-item :index="`/${id()}/hashtag`" role="menuitem" :title="$t('side_menu.hashtag')">
<icon name="hashtag"></icon> <icon name="hashtag"></icon>
<span>{{ $t("side_menu.hashtag") }}</span> <span>{{ $t('side_menu.hashtag') }}</span>
</el-menu-item> </el-menu-item>
<template v-for="tag in tags"> <template v-for="tag in tags">
<el-menu-item :index="`/${id()}/hashtag/${tag.tagName}`" :class="collapse ? '' : 'sub-menu'" :key="tag.tagName" role="menuitem" :title="tag.tagName"> <el-menu-item
:index="`/${id()}/hashtag/${tag.tagName}`"
:class="collapse ? '' : 'sub-menu'"
:key="tag.tagName"
role="menuitem"
:title="tag.tagName"
>
<icon name="hashtag" scale="0.8"></icon> <icon name="hashtag" scale="0.8"></icon>
<span>{{ tag.tagName }}</span> <span>{{ tag.tagName }}</span>
</el-menu-item> </el-menu-item>
</template> </template>
<el-menu-item :index="`/${id()}/lists`" role="menuitem" :title="$t('side_menu.lists')"> <el-menu-item :index="`/${id()}/lists`" role="menuitem" :title="$t('side_menu.lists')">
<icon name="list-ul"></icon> <icon name="list-ul"></icon>
<span>{{ $t("side_menu.lists") }}</span> <span>{{ $t('side_menu.lists') }}</span>
</el-menu-item> </el-menu-item>
<template v-for="list in lists"> <template v-for="list in lists">
<el-menu-item :index="`/${id()}/lists/${list.id}`" :class="collapse ? '' : 'sub-menu'" :key="list.id" role="menuitem" :title="list.title"> <el-menu-item
:index="`/${id()}/lists/${list.id}`"
:class="collapse ? '' : 'sub-menu'"
:key="list.id"
role="menuitem"
:title="list.title"
>
<icon name="list-ul" scale="0.8"></icon> <icon name="list-ul" scale="0.8"></icon>
<span>{{ list.title }}</span> <span>{{ list.title }}</span>
</el-menu-item> </el-menu-item>
@ -128,6 +145,7 @@ export default {
unreadLocalTimeline: state => state.unreadLocalTimeline, unreadLocalTimeline: state => state.unreadLocalTimeline,
unreadDirectMessagesTimeline: state => state.unreadDirectMessagesTimeline, unreadDirectMessagesTimeline: state => state.unreadDirectMessagesTimeline,
unreadPublicTimeline: state => state.unreadPublicTimeline, unreadPublicTimeline: state => state.unreadPublicTimeline,
unreadFollowRequests: state => state.unreadFollowRequests,
lists: state => state.lists, lists: state => state.lists,
tags: state => state.tags, tags: state => state.tags,
collapse: state => state.collapse collapse: state => state.collapse
@ -138,22 +156,21 @@ export default {
hideGlobalHeader: state => state.GlobalHeader.hide hideGlobalHeader: state => state.GlobalHeader.hide
}) })
}, },
created () { created() {
this.$store.dispatch('TimelineSpace/SideMenu/readCollapse') this.$store.dispatch('TimelineSpace/SideMenu/readCollapse')
this.$store.dispatch('TimelineSpace/SideMenu/listTags') this.$store.dispatch('TimelineSpace/SideMenu/listTags')
}, },
methods: { methods: {
activeRoute () { activeRoute() {
return this.$route.path return this.$route.path
}, },
id () { id() {
return this.$route.params.id return this.$route.params.id
}, },
handleProfile (command) { handleProfile(command) {
switch (command) { switch (command) {
case 'show': case 'show':
this.$store.dispatch('TimelineSpace/Contents/SideBar/AccountProfile/fetchAccount', this.account.accountId) this.$store.dispatch('TimelineSpace/Contents/SideBar/AccountProfile/fetchAccount', this.account.accountId).then(account => {
.then((account) => {
this.$store.dispatch('TimelineSpace/Contents/SideBar/AccountProfile/changeAccount', account) this.$store.dispatch('TimelineSpace/Contents/SideBar/AccountProfile/changeAccount', account)
this.$store.commit('TimelineSpace/Contents/SideBar/changeOpenSideBar', true) this.$store.commit('TimelineSpace/Contents/SideBar/changeOpenSideBar', true)
}) })
@ -169,13 +186,13 @@ export default {
break break
} }
}, },
doCollapse () { doCollapse() {
this.$store.dispatch('TimelineSpace/SideMenu/changeCollapse', true) this.$store.dispatch('TimelineSpace/SideMenu/changeCollapse', true)
}, },
releaseCollapse () { releaseCollapse() {
this.$store.dispatch('TimelineSpace/SideMenu/changeCollapse', false) this.$store.dispatch('TimelineSpace/SideMenu/changeCollapse', false)
}, },
async changeGlobalHeader (value) { async changeGlobalHeader(value) {
await this.$store.dispatch('GlobalHeader/switchHide', value) await this.$store.dispatch('GlobalHeader/switchHide', value)
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="user" @click="openUser(user)" aria-label="user"> <div class="user" @click="openUser(user)" aria-label="user">
<div class="icon" role="presentation"> <div class="icon" role="presentation">
<FailoverImg :src="user.avatar" :alt="`Avatar of ${user.username}`" /> <FailoverImg :src="user.avatar" :alt="`Avatar of ${user.username}`" />
</div> </div>
@ -7,9 +7,7 @@
<div class="username"> <div class="username">
<bdi v-html="username(user)"></bdi> <bdi v-html="username(user)"></bdi>
</div> </div>
<div class="acct"> <div class="acct">@{{ user.acct }}</div>
@{{ user.acct }}
</div>
</div> </div>
<div class="tool" v-if="remove"> <div class="tool" v-if="remove">
<el-button type="text" @click.stop.prevent="removeAccount(user)"> <el-button type="text" @click.stop.prevent="removeAccount(user)">
@ -17,18 +15,37 @@
</el-button> </el-button>
</div> </div>
<div class="tool" v-if="relationship"> <div class="tool" v-if="relationship">
<el-button v-if="relationship.following" class="unfollow" type="text" @click.stop.prevent="unfollowAccount(user)" :title="$t('side_bar.account_profile.unfollow')"> <el-button
v-if="relationship.following"
class="unfollow"
type="text"
@click.stop.prevent="unfollowAccount(user)"
:title="$t('side_bar.account_profile.unfollow')"
>
<icon name="user-times"></icon> <icon name="user-times"></icon>
</el-button> </el-button>
<el-button v-else-if="relationship.requested" class="requested" type="text" :title="$t('side_bar.account_profile.follow_requested')"> <el-button v-else-if="relationship.requested" class="requested" type="text" :title="$t('side_bar.account_profile.follow_requested')">
<icon name="hourglass"></icon> <icon name="hourglass"></icon>
</el-button> </el-button>
<el-button v-else-if="!relationship.following" class="follow" type="text" @click.stop.prevent="followAccount(user)" :title="$t('side_bar.account_profile.follow')"> <el-button
v-else-if="!relationship.following"
class="follow"
type="text"
@click.stop.prevent="followAccount(user)"
:title="$t('side_bar.account_profile.follow')"
>
<icon name="user-plus"></icon> <icon name="user-plus"></icon>
</el-button> </el-button>
</div> </div>
<div class="tool" v-else-if="request">
<el-button class="accept" type="text" @click.stop.prevent="acceptRequest(user)" :title="$t('follow_requests.accept')">
<icon name="check"></icon>
</el-button>
<el-button class="reject" type="text" @click.stop.prevent="rejectRequest(user)" :tilte="$t('follow_requests.reject')">
<icon name="times"></icon>
</el-button>
</div>
</div> </div>
</div>
</template> </template>
<script> <script>
@ -52,29 +69,39 @@ export default {
relationship: { relationship: {
type: Object, type: Object,
default: null default: null
},
request: {
type: Boolean,
default: false
} }
}, },
methods: { methods: {
username (account) { username(account) {
if (account.display_name !== '') { if (account.display_name !== '') {
return emojify(account.display_name, account.emojis) return emojify(account.display_name, account.emojis)
} else { } else {
return account.username return account.username
} }
}, },
openUser (account) { openUser(account) {
this.$store.dispatch('TimelineSpace/Contents/SideBar/openAccountComponent') this.$store.dispatch('TimelineSpace/Contents/SideBar/openAccountComponent')
this.$store.dispatch('TimelineSpace/Contents/SideBar/AccountProfile/changeAccount', account) this.$store.dispatch('TimelineSpace/Contents/SideBar/AccountProfile/changeAccount', account)
this.$store.commit('TimelineSpace/Contents/SideBar/changeOpenSideBar', true) this.$store.commit('TimelineSpace/Contents/SideBar/changeOpenSideBar', true)
}, },
removeAccount (account) { removeAccount(account) {
this.$emit('removeAccount', account) this.$emit('removeAccount', account)
}, },
unfollowAccount (account) { unfollowAccount(account) {
this.$emit('unfollowAccount', account) this.$emit('unfollowAccount', account)
}, },
followAccount (account) { followAccount(account) {
this.$emit('followAccount', account) this.$emit('followAccount', account)
},
acceptRequest(account) {
this.$emit('acceptRequest', account)
},
rejectRequest(account) {
this.$emit('rejectRequest', account)
} }
} }
} }
@ -151,6 +178,11 @@ export default {
padding-top: 8px; padding-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
} }
.accept,
.reject {
margin-right: 24px;
}
} }
} }
</style> </style>

View File

@ -14,7 +14,7 @@ const router = new Router({
path: '/authorize', path: '/authorize',
name: 'authorize', name: 'authorize',
component: require('@/components/Authorize').default, component: require('@/components/Authorize').default,
props: (route) => ({ url: route.query.url }) props: route => ({ url: route.query.url })
}, },
{ {
path: '/preferences/', path: '/preferences/',
@ -87,6 +87,11 @@ const router = new Router({
name: 'mentions', name: 'mentions',
component: require('@/components/TimelineSpace/Contents/Mentions').default component: require('@/components/TimelineSpace/Contents/Mentions').default
}, },
{
path: 'follow-requests',
name: 'follow-requests',
component: require('@/components/TimelineSpace/Contents/FollowRequests').default
},
{ {
path: 'favourites', path: 'favourites',
name: 'favourites', name: 'favourites',

View File

@ -8,6 +8,7 @@ import Search, { SearchModuleState } from './Contents/Search'
import Lists from './Contents/Lists' import Lists from './Contents/Lists'
import Hashtag, { HashtagModuleState } from './Contents/Hashtag' import Hashtag, { HashtagModuleState } from './Contents/Hashtag'
import DirectMessages, { DirectMessagesState } from './Contents/DirectMessages' import DirectMessages, { DirectMessagesState } from './Contents/DirectMessages'
import FollowRequests, { FollowRequestsState } from './Contents/FollowRequests'
import Mentions, { MentionsState } from './Contents/Mentions' import Mentions, { MentionsState } from './Contents/Mentions'
import { Module } from 'vuex' import { Module } from 'vuex'
import { RootState } from '@/store' import { RootState } from '@/store'
@ -15,16 +16,17 @@ import { RootState } from '@/store'
export interface ContentsState {} export interface ContentsState {}
export interface ContentsModuleState extends ContentsState { export interface ContentsModuleState extends ContentsState {
SideBar: SideBarModuleState, SideBar: SideBarModuleState
Home: HomeState, Home: HomeState
Notifications: NotificationsState, Notifications: NotificationsState
Mentions: MentionsState, Mentions: MentionsState
DirectMessages: DirectMessagesState, DirectMessages: DirectMessagesState
Favourites: FavouritesState, Favourites: FavouritesState
Local: LocalState, Local: LocalState
Public: PublicState, Public: PublicState
Search: SearchModuleState, Search: SearchModuleState
Hashtag: HashtagModuleState Hashtag: HashtagModuleState
FollowRequests: FollowRequestsState
} }
const state = (): ContentsState => ({}) const state = (): ContentsState => ({})
@ -43,7 +45,8 @@ const Contents: Module<ContentsState, RootState> = {
Public, Public,
Search, Search,
Lists, Lists,
Hashtag Hashtag,
FollowRequests
} }
} }

View File

@ -0,0 +1,53 @@
import Mastodon, { Account, Response } from 'megalodon'
import { Module, MutationTree, ActionTree } from 'vuex'
import { RootState } from '@/store'
export interface FollowRequestsState {
requests: Array<Account>
}
const state = (): FollowRequestsState => ({
requests: []
})
export const MUTATION_TYPES = {
UPDATE_REQUESTS: 'updateRequests'
}
const mutations: MutationTree<FollowRequestsState> = {
[MUTATION_TYPES.UPDATE_REQUESTS]: (state, accounts: Array<Account>) => {
state.requests = accounts
}
}
const actions: ActionTree<FollowRequestsState, RootState> = {
fetchRequests: async ({ commit, rootState }): Promise<Array<Account>> => {
const client = new Mastodon(rootState.TimelineSpace.account.accessToken!, rootState.TimelineSpace.account.baseURL + '/api/v1')
const res: Response<Array<Account>> = await client.get<Array<Account>>('/follow_requests')
commit(MUTATION_TYPES.UPDATE_REQUESTS, res.data)
return res.data
},
acceptRequest: async ({ dispatch, rootState }, user: Account) => {
const client = new Mastodon(rootState.TimelineSpace.account.accessToken!, rootState.TimelineSpace.account.baseURL + '/api/v1')
const res: Response<{}> = await client.post<{}>(`/follow_requests/${user.id}/authorize`)
dispatch('fetchRequests')
dispatch('TimelineSpace/SideMenu/fetchFollowRequests', rootState.TimelineSpace.account, { root: true })
return res.data
},
rejectRequest: async ({ dispatch, rootState }, user: Account) => {
const client = new Mastodon(rootState.TimelineSpace.account.accessToken!, rootState.TimelineSpace.account.baseURL + '/api/v1')
const res: Response<{}> = await client.post<{}>(`/follow_requests/${user.id}/reject`)
dispatch('fetchRequests')
dispatch('TimelineSpace/SideMenu/fetchFollowRequests', rootState.TimelineSpace.account, { root: true })
return res.data
}
}
const FollowRequests: Module<FollowRequestsState, RootState> = {
namespaced: true,
state: state,
mutations: mutations,
actions: actions
}
export default FollowRequests

View File

@ -1,4 +1,4 @@
import Mastodon, { List, Response } from 'megalodon' import Mastodon, { List, Response, Account } from 'megalodon'
import { ipcRenderer } from 'electron' import { ipcRenderer } from 'electron'
import { Module, MutationTree, ActionTree } from 'vuex' import { Module, MutationTree, ActionTree } from 'vuex'
import LocalTag from '~/src/types/localTag' import LocalTag from '~/src/types/localTag'
@ -6,14 +6,15 @@ import LocalAccount from '~/src/types/localAccount'
import { RootState } from '@/store' import { RootState } from '@/store'
export interface SideMenuState { export interface SideMenuState {
unreadHomeTimeline: boolean, unreadHomeTimeline: boolean
unreadNotifications: boolean, unreadNotifications: boolean
unreadMentions: boolean, unreadMentions: boolean
unreadLocalTimeline: boolean, unreadLocalTimeline: boolean
unreadDirectMessagesTimeline: boolean, unreadDirectMessagesTimeline: boolean
unreadPublicTimeline: boolean, unreadPublicTimeline: boolean
lists: Array<List>, unreadFollowRequests: boolean
tags: Array<LocalTag>, lists: Array<List>
tags: Array<LocalTag>
collapse: boolean collapse: boolean
} }
@ -24,6 +25,7 @@ const state = (): SideMenuState => ({
unreadLocalTimeline: false, unreadLocalTimeline: false,
unreadDirectMessagesTimeline: false, unreadDirectMessagesTimeline: false,
unreadPublicTimeline: false, unreadPublicTimeline: false,
unreadFollowRequests: false,
lists: [], lists: [],
tags: [], tags: [],
collapse: false collapse: false
@ -36,6 +38,7 @@ export const MUTATION_TYPES = {
CHANGE_UNREAD_LOCAL_TIMELINE: 'changeUnreadLocalTimeline', CHANGE_UNREAD_LOCAL_TIMELINE: 'changeUnreadLocalTimeline',
CHANGE_UNREAD_DIRECT_MESSAGES_TIMELINE: 'changeUnreadDirectMessagesTimeline', CHANGE_UNREAD_DIRECT_MESSAGES_TIMELINE: 'changeUnreadDirectMessagesTimeline',
CHANGE_UNREAD_PUBLIC_TIMELINE: 'changeUnreadPublicTimeline', CHANGE_UNREAD_PUBLIC_TIMELINE: 'changeUnreadPublicTimeline',
CHANGE_UNREAD_FOLLOW_REQUESTS: 'changeUnreadFollowRequests',
UPDATE_LISTS: 'updateLists', UPDATE_LISTS: 'updateLists',
CHANGE_COLLAPSE: 'changeCollapse', CHANGE_COLLAPSE: 'changeCollapse',
UPDATE_TAGS: 'updateTags' UPDATE_TAGS: 'updateTags'
@ -60,6 +63,9 @@ const mutations: MutationTree<SideMenuState> = {
[MUTATION_TYPES.CHANGE_UNREAD_PUBLIC_TIMELINE]: (state, value: boolean) => { [MUTATION_TYPES.CHANGE_UNREAD_PUBLIC_TIMELINE]: (state, value: boolean) => {
state.unreadPublicTimeline = value state.unreadPublicTimeline = value
}, },
[MUTATION_TYPES.CHANGE_UNREAD_FOLLOW_REQUESTS]: (state, value: boolean) => {
state.unreadFollowRequests = value
},
[MUTATION_TYPES.UPDATE_LISTS]: (state, lists: Array<List>) => { [MUTATION_TYPES.UPDATE_LISTS]: (state, lists: Array<List>) => {
state.lists = lists state.lists = lists
}, },
@ -74,14 +80,18 @@ const mutations: MutationTree<SideMenuState> = {
const actions: ActionTree<SideMenuState, RootState> = { const actions: ActionTree<SideMenuState, RootState> = {
fetchLists: async ({ commit, rootState }, account: LocalAccount | null = null): Promise<Array<List>> => { fetchLists: async ({ commit, rootState }, account: LocalAccount | null = null): Promise<Array<List>> => {
if (account === null) account = rootState.TimelineSpace.account if (account === null) account = rootState.TimelineSpace.account
const client = new Mastodon( const client = new Mastodon(account!.accessToken!, account!.baseURL + '/api/v1')
account!.accessToken!,
account!.baseURL + '/api/v1'
)
const res: Response<Array<List>> = await client.get<Array<List>>('/lists') const res: Response<Array<List>> = await client.get<Array<List>>('/lists')
commit(MUTATION_TYPES.UPDATE_LISTS, res.data) commit(MUTATION_TYPES.UPDATE_LISTS, res.data)
return res.data return res.data
}, },
fetchFollowRequests: async ({ commit, rootState }, account: LocalAccount | null = null): Promise<Array<Account>> => {
if (account === null) account = rootState.TimelineSpace.account
const client = new Mastodon(account!.accessToken!, account!.baseURL + '/api/v1')
const res: Response<Array<Account>> = await client.get<Array<Account>>('/follow_requests')
commit(MUTATION_TYPES.CHANGE_UNREAD_FOLLOW_REQUESTS, res.data.length > 0)
return res.data
},
clearUnread: ({ commit }) => { clearUnread: ({ commit }) => {
commit(MUTATION_TYPES.CHANGE_UNREAD_HOME_TIMELINE, false) commit(MUTATION_TYPES.CHANGE_UNREAD_HOME_TIMELINE, false)
commit(MUTATION_TYPES.CHANGE_UNREAD_NOTIFICATIONS, false) commit(MUTATION_TYPES.CHANGE_UNREAD_NOTIFICATIONS, false)