diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 4650f03a29..a77991cccd 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -468,6 +468,10 @@ unableToProcess: "操作を完了できません" recentUsed: "最近使用" install: "インストール" uninstall: "アンインストール" +installedApps: "インストールされたアプリ" +nothing: "ありません" +installedDate: "インストール日時" +lastUsedDate: "最終使用日時" _theme: explore: "テーマを探す" diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue index 65dda17575..0cf4dee2dd 100644 --- a/src/client/components/notes.vue +++ b/src/client/components/notes.vue @@ -1,6 +1,6 @@ <template> <div class="mk-notes" v-size="[{ max: 500 }]"> - <div class="empty" v-if="empty"> + <div class="_fullinfo" v-if="empty"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <div>{{ $t('noNotes') }}</div> </div> @@ -90,18 +90,6 @@ export default Vue.extend({ <style lang="scss" scoped> .mk-notes { - > .empty { - padding: 32px; - text-align: center; - - > img { - vertical-align: bottom; - height: 128px; - margin-bottom: 16px; - border-radius: 16px; - } - } - > .notes { > ::v-deep *:not(:last-child) { margin-bottom: var(--marginFull); diff --git a/src/client/pages/apps.vue b/src/client/pages/apps.vue new file mode 100644 index 0000000000..e25bffa2bc --- /dev/null +++ b/src/client/pages/apps.vue @@ -0,0 +1,98 @@ +<template> +<div> + <portal to="icon"><fa :icon="faPlug"/></portal> + <portal to="title">{{ $t('installedApps') }}</portal> + + <mk-pagination :pagination="pagination" class="bfomjevm" ref="list"> + <template #empty> + <div class="_fullinfo"> + <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <div>{{ $t('nothing') }}</div> + </div> + </template> + <template #default="{items}"> + <div class="token _panel" v-for="token in items" :key="token.id"> + <img class="icon" :src="token.iconUrl" alt=""/> + <div class="body"> + <div class="name">{{ token.name }}</div> + <div class="description">{{ token.description }}</div> + <div class="_keyValue"> + <div>{{ $t('installedDate') }}:</div> + <div><mk-time :time="token.createdAt"/></div> + </div> + <div class="_keyValue"> + <div>{{ $t('lastUsedDate') }}:</div> + <div><mk-time :time="token.lastUsedAt"/></div> + </div> + <div class="actions"> + <button class="_button" @click="revoke(token)"><fa :icon="faTrashAlt"/></button> + </div> + </div> + </div> + </template> + </mk-pagination> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faTrashAlt, faPlug } from '@fortawesome/free-solid-svg-icons'; +import MkPagination from '../components/ui/pagination.vue'; + +export default Vue.extend({ + metaInfo() { + return { + title: this.$t('installedApps') as string + }; + }, + + components: { + MkPagination + }, + + data() { + return { + pagination: { + endpoint: 'i/apps', + limit: 100, + }, + faTrashAlt, faPlug + }; + }, + + methods: { + revoke(token) { + this.$root.api('i/revoke-token', { tokenId: token.id }).then(() => { + this.$refs.list.reload(); + }); + } + } +}); +</script> + +<style lang="scss" scoped> +.bfomjevm { + > .token { + display: flex; + padding: 16px; + + > .icon { + display: block; + flex-shrink: 0; + margin: 0 12px 0 0; + width: 50px; + height: 50px; + border-radius: 8px; + } + + > .body { + width: calc(100% - 62px); + position: relative; + + > .name { + font-weight: bold; + } + } + } +} +</style> diff --git a/src/client/pages/follow-requests.vue b/src/client/pages/follow-requests.vue index a900bf735c..b310d9f581 100644 --- a/src/client/pages/follow-requests.vue +++ b/src/client/pages/follow-requests.vue @@ -5,7 +5,7 @@ <mk-pagination :pagination="pagination" class="mk-follow-requests" ref="list"> <template #empty> - <div class="tkdrhpxr"> + <div class="_fullinfo"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <div>{{ $t('noFollowRequests') }}</div> </div> @@ -75,18 +75,6 @@ export default Vue.extend({ <style lang="scss" scoped> .mk-follow-requests { - .tkdrhpxr { - padding: 32px; - text-align: center; - - > img { - vertical-align: bottom; - height: 128px; - margin-bottom: 16px; - border-radius: 16px; - } - } - > .user { display: flex; padding: 16px; diff --git a/src/client/pages/messaging/index.vue b/src/client/pages/messaging/index.vue index ed24f8ef54..7a55004cbf 100644 --- a/src/client/pages/messaging/index.vue +++ b/src/client/pages/messaging/index.vue @@ -31,7 +31,7 @@ </div> </router-link> </div> - <div class="no-history" v-if="!fetching && messages.length == 0"> + <div class="_fullinfo" v-if="!fetching && messages.length == 0"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <div>{{ $t('noHistory') }}</div> </div> @@ -287,18 +287,6 @@ export default Vue.extend({ } } - > .no-history { - padding: 32px; - text-align: center; - - > img { - vertical-align: bottom; - height: 128px; - margin-bottom: 16px; - border-radius: 16px; - } - } - @media (max-width: 400px) { > .history { > .message { diff --git a/src/client/pages/my-settings/index.vue b/src/client/pages/my-settings/index.vue index 4742793f2b..c3080e0f81 100644 --- a/src/client/pages/my-settings/index.vue +++ b/src/client/pages/my-settings/index.vue @@ -32,7 +32,9 @@ <x-integration/> <x-api/> - <mk-button @click="$root.signout()" primary style="margin: var(--margin) auto;">{{ $t('logout') }}</mk-button> + <router-link class="_panel _buttonPrimary" to="/my/apps" style="margin: var(--margin) auto;">{{ $t('installedApps') }}</router-link> + + <button class="_panel _buttonPrimary" @click="$root.signout()" style="margin: var(--margin) auto;">{{ $t('logout') }}</button> </div> </template> diff --git a/src/client/router.ts b/src/client/router.ts index 70d497d36f..9644ede55f 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -46,6 +46,7 @@ export const router = new VueRouter({ { path: '/my/groups', component: page('my-groups/index') }, { path: '/my/groups/:group', component: page('my-groups/group') }, { path: '/my/antennas', component: page('my-antennas/index') }, + { path: '/my/apps', component: page('apps') }, { path: '/preferences', component: page('preferences/index') }, { path: '/instance', component: page('instance/index') }, { path: '/instance/emojis', component: page('instance/emojis') }, diff --git a/src/client/style.scss b/src/client/style.scss index 7b509e5b51..57906d5ae7 100644 --- a/src/client/style.scss +++ b/src/client/style.scss @@ -412,6 +412,26 @@ main ._panel { } } +._fullinfo { + padding: 32px; + text-align: center; + + > img { + vertical-align: bottom; + height: 128px; + margin-bottom: 16px; + border-radius: 16px; + } +} + +._keyValue { + display: flex; + + > div { + flex: 1; + } +} + ._link { color: var(--link); } diff --git a/src/server/api/endpoints/i/revoke-token.ts b/src/server/api/endpoints/i/revoke-token.ts new file mode 100644 index 0000000000..ce688c5755 --- /dev/null +++ b/src/server/api/endpoints/i/revoke-token.ts @@ -0,0 +1,24 @@ +import $ from 'cafy'; +import define from '../../define'; +import { AccessTokens } from '../../../../models'; +import { ID } from '../../../../misc/cafy-id'; + +export const meta = { + requireCredential: true as const, + + secure: true, + + params: { + tokenId: { + validator: $.type(ID) + } + } +}; + +export default define(meta, async (ps, user) => { + const token = await AccessTokens.findOne(ps.tokenId); + + if (token) { + AccessTokens.delete(token.id); + } +});