summaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/components/notes.vue14
-rw-r--r--src/client/components/notification.vue35
-rw-r--r--src/client/pages/apps.vue101
-rwxr-xr-xsrc/client/pages/auth.vue8
-rw-r--r--src/client/pages/doc.vue25
-rw-r--r--src/client/pages/follow-requests.vue14
-rw-r--r--src/client/pages/messaging/index.vue14
-rw-r--r--src/client/pages/miauth.vue103
-rw-r--r--src/client/pages/my-settings/index.vue4
-rw-r--r--src/client/pages/preferences/theme.vue28
-rw-r--r--src/client/router.ts2
-rw-r--r--src/client/style.scss20
12 files changed, 303 insertions, 65 deletions
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/components/notification.vue b/src/client/components/notification.vue
index d768c0b074..f415887e76 100644
--- a/src/client/components/notification.vue
+++ b/src/client/components/notification.vue
@@ -1,22 +1,24 @@
<template>
<div class="mk-notification" :class="notification.type" v-size="[{ max: 500 }, { max: 600 }]">
<div class="head">
- <mk-avatar class="avatar" :user="notification.user"/>
- <div class="icon" :class="notification.type">
+ <mk-avatar v-if="notification.user" class="icon" :user="notification.user"/>
+ <img v-else class="icon" :src="notification.icon" alt=""/>
+ <div class="sub-icon" :class="notification.type">
<fa :icon="faPlus" v-if="notification.type === 'follow'"/>
- <fa :icon="faClock" v-if="notification.type === 'receiveFollowRequest'"/>
- <fa :icon="faCheck" v-if="notification.type === 'followRequestAccepted'"/>
- <fa :icon="faIdCardAlt" v-if="notification.type === 'groupInvited'"/>
- <fa :icon="faRetweet" v-if="notification.type === 'renote'"/>
- <fa :icon="faReply" v-if="notification.type === 'reply'"/>
- <fa :icon="faAt" v-if="notification.type === 'mention'"/>
- <fa :icon="faQuoteLeft" v-if="notification.type === 'quote'"/>
- <x-reaction-icon v-if="notification.type === 'reaction'" :reaction="notification.reaction" :no-style="true"/>
+ <fa :icon="faClock" v-else-if="notification.type === 'receiveFollowRequest'"/>
+ <fa :icon="faCheck" v-else-if="notification.type === 'followRequestAccepted'"/>
+ <fa :icon="faIdCardAlt" v-else-if="notification.type === 'groupInvited'"/>
+ <fa :icon="faRetweet" v-else-if="notification.type === 'renote'"/>
+ <fa :icon="faReply" v-else-if="notification.type === 'reply'"/>
+ <fa :icon="faAt" v-else-if="notification.type === 'mention'"/>
+ <fa :icon="faQuoteLeft" v-else-if="notification.type === 'quote'"/>
+ <x-reaction-icon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :no-style="true"/>
</div>
</div>
<div class="tail">
<header>
- <router-link class="name" :to="notification.user | userPage" v-user-preview="notification.user.id"><mk-user-name :user="notification.user"/></router-link>
+ <router-link v-if="notification.user" class="name" :to="notification.user | userPage" v-user-preview="notification.user.id"><mk-user-name :user="notification.user"/></router-link>
+ <span v-else>{{ notification.header }}</span>
<mk-time :time="notification.createdAt" v-if="withTime"/>
</header>
<router-link v-if="notification.type === 'reaction'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
@@ -42,6 +44,9 @@
<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $t('followRequestAccepted') }}</span>
<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $t('receiveFollowRequest') }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $t('reject') }}</button></div></span>
<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ $t('groupInvited') }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ $t('reject') }}</button></div></span>
+ <span v-if="notification.type === 'app'" class="text">
+ <mfm :text="notification.body" :nowrap="!full"/>
+ </span>
</div>
</div>
</template>
@@ -142,14 +147,14 @@ export default Vue.extend({
height: 42px;
margin-right: 8px;
- > .avatar {
+ > .icon {
display: block;
width: 100%;
height: 100%;
border-radius: 6px;
}
- > .icon {
+ > .sub-icon {
position: absolute;
z-index: 1;
bottom: -2px;
@@ -163,6 +168,10 @@ export default Vue.extend({
font-size: 12px;
pointer-events: none;
+ &:empty {
+ display: none;
+ }
+
> * {
color: #fff;
width: 100%;
diff --git a/src/client/pages/apps.vue b/src/client/pages/apps.vue
new file mode 100644
index 0000000000..03c6707f95
--- /dev/null
+++ b/src/client/pages/apps.vue
@@ -0,0 +1,101 @@
+<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,
+ params: {
+ sort: '+lastUsedAt'
+ }
+ },
+ 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/auth.vue b/src/client/pages/auth.vue
index 9f5b45f001..e025924fe0 100755
--- a/src/client/pages/auth.vue
+++ b/src/client/pages/auth.vue
@@ -12,20 +12,18 @@
@accepted="accepted"
/>
<div class="denied _panel" v-if="state == 'denied'">
- <h1>{{ $t('denied') }}</h1>
- <p>{{ $t('denied-paragraph') }}</p>
+ <h1>{{ $t('_auth.denied') }}</h1>
</div>
<div class="accepted _panel" v-if="state == 'accepted'">
<h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$t('allowed') }}</h1>
- <p v-if="session.app.callbackUrl">{{ $t('callback-url') }}<mk-ellipsis/></p>
- <p v-if="!session.app.callbackUrl">{{ $t('please-go-back') }}</p>
+ <p v-if="session.app.callbackUrl">{{ $t('_auth.callback') }}<mk-ellipsis/></p>
+ <p v-if="!session.app.callbackUrl">{{ $t('_auth.pleaseGoBack') }}</p>
</div>
<div class="error _panel" v-if="state == 'fetch-session-error'">
<p>{{ $t('error') }}</p>
</div>
</div>
<div class="signin" v-else>
- <h1>{{ $t('sign-in') }}</h1>
<mk-signin @login="onLogin"/>
</div>
</template>
diff --git a/src/client/pages/doc.vue b/src/client/pages/doc.vue
index e0db5a3746..7c4f7ebccf 100644
--- a/src/client/pages/doc.vue
+++ b/src/client/pages/doc.vue
@@ -18,6 +18,7 @@
import Vue from 'vue';
import { faFileAlt } from '@fortawesome/free-solid-svg-icons'
import MarkdownIt from 'markdown-it';
+import MarkdownItAnchor from 'markdown-it-anchor';
import i18n from '../i18n';
import { url, lang } from '../config';
import MkLink from '../components/link.vue';
@@ -26,6 +27,10 @@ const markdown = MarkdownIt({
html: true
});
+markdown.use(MarkdownItAnchor, {
+ slugify: (s) => encodeURIComponent(String(s).trim().replace(/\s+/g, '-'))
+});
+
export default Vue.extend({
i18n,
@@ -72,6 +77,9 @@ export default Vue.extend({
},
parse(md: string) {
+ // 変数置換
+ md = md.replace(/\{_URL_\}/g, url);
+
// markdown の全容をパースする
const parsed = markdown.parse(md, {});
if (parsed.length === 0) return;
@@ -115,6 +123,23 @@ export default Vue.extend({
margin-bottom: 0;
}
+ ::v-deep a {
+ color: var(--link);
+ }
+
+ ::v-deep blockquote {
+ display: block;
+ margin: 8px;
+ padding: 6px 0 6px 12px;
+ color: var(--fg);
+ border-left: solid 3px var(--fg);
+ opacity: 0.7;
+
+ p {
+ margin: 0;
+ }
+ }
+
::v-deep h2 {
font-size: 1.25em;
padding: 0 0 0.5em 0;
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/miauth.vue b/src/client/pages/miauth.vue
new file mode 100644
index 0000000000..2ee0f23479
--- /dev/null
+++ b/src/client/pages/miauth.vue
@@ -0,0 +1,103 @@
+<template>
+<div v-if="$store.getters.isSignedIn">
+ <div class="waiting _card" v-if="state == 'waiting'">
+ <div class="_content">
+ <mk-loading/>
+ </div>
+ </div>
+ <div class="denied _card" v-if="state == 'denied'">
+ <div class="_content">
+ <p>{{ $t('_auth.denied') }}</p>
+ </div>
+ </div>
+ <div class="accepted _card" v-else-if="state == 'accepted'">
+ <div class="_content">
+ <p v-if="callback">{{ $t('_auth.callback') }}<mk-ellipsis/></p>
+ <p v-else>{{ $t('_auth.pleaseGoBack') }}</p>
+ </div>
+ </div>
+ <div class="_card" v-else>
+ <div class="_title" v-if="name">{{ $t('_auth.shareAccess', { name: name }) }}</div>
+ <div class="_title" v-else>{{ $t('_auth.shareAccessAsk') }}</div>
+ <div class="_content">
+ <p>{{ $t('_auth.permissionAsk') }}</p>
+ <ul>
+ <template v-for="p in permission">
+ <li :key="p">{{ $t(`_permissions.${p}`) }}</li>
+ </template>
+ </ul>
+ </div>
+ <div class="_footer">
+ <mk-button @click="deny" inline>{{ $t('cancel') }}</mk-button>
+ <mk-button @click="accept" inline primary>{{ $t('accept') }}</mk-button>
+ </div>
+ </div>
+</div>
+<div class="signin" v-else>
+ <mk-signin @login="onLogin"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import MkSignin from '../components/signin.vue';
+import MkButton from '../components/ui/button.vue';
+
+export default Vue.extend({
+ i18n,
+ components: {
+ MkSignin,
+ MkButton,
+ },
+ data() {
+ return {
+ state: null
+ };
+ },
+ computed: {
+ session(): string {
+ return this.$route.params.session;
+ },
+ callback(): string {
+ return this.$route.query.callback;
+ },
+ name(): string {
+ return this.$route.query.name;
+ },
+ icon(): string {
+ return this.$route.query.icon;
+ },
+ permission(): string {
+ return this.$route.query.permission;
+ },
+ },
+ methods: {
+ async accept() {
+ this.state = 'waiting';
+ await this.$root.api('miauth/gen-token', {
+ session: this.session,
+ name: this.name,
+ iconUrl: this.icon,
+ permission: this.permission || [],
+ });
+
+ this.state = 'accepted';
+ if (this.callback) {
+ location.href = `${this.callback}?session=${this.session}`;
+ }
+ },
+ deny() {
+ this.state = 'denied';
+ },
+ onLogin(res) {
+ localStorage.setItem('i', res.i);
+ location.reload();
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
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/pages/preferences/theme.vue b/src/client/pages/preferences/theme.vue
index fcea457396..f35b5d6ed8 100644
--- a/src/client/pages/preferences/theme.vue
+++ b/src/client/pages/preferences/theme.vue
@@ -57,7 +57,8 @@
<mk-textarea v-model="installThemeCode">
<span>{{ $t('_theme.code') }}</span>
</mk-textarea>
- <mk-button @click="() => install(this.installThemeCode)" :disabled="installThemeCode == null"><fa :icon="faCheck"/> {{ $t('install') }}</mk-button>
+ <mk-button @click="() => install(this.installThemeCode)" :disabled="installThemeCode == null" primary inline><fa :icon="faCheck"/> {{ $t('install') }}</mk-button>
+ <mk-button @click="() => preview(this.installThemeCode)" :disabled="installThemeCode == null" inline><fa :icon="faEye"/> {{ $t('preview') }}</mk-button>
</details>
</div>
<div class="_content">
@@ -79,7 +80,7 @@
<script lang="ts">
import Vue from 'vue';
-import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
+import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
import * as JSON5 from 'json5';
import MkInput from '../../components/ui/input.vue';
import MkButton from '../../components/ui/button.vue';
@@ -108,7 +109,7 @@ export default Vue.extend({
installThemeCode: null,
selectedThemeId: null,
wallpaper: localStorage.getItem('wallpaper'),
- faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt
+ faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
}
},
@@ -196,8 +197,9 @@ export default Vue.extend({
});
},
- install(code) {
+ parseThemeCode(code) {
let theme;
+
try {
theme = JSON5.parse(code);
} catch (e) {
@@ -205,22 +207,34 @@ export default Vue.extend({
type: 'error',
text: this.$t('_theme.invalid')
});
- return;
+ return false;
}
if (!validateTheme(theme)) {
this.$root.dialog({
type: 'error',
text: this.$t('_theme.invalid')
});
- return;
+ return false;
}
if (this.$store.state.device.themes.some(t => t.id === theme.id)) {
this.$root.dialog({
type: 'info',
text: this.$t('_theme.alreadyInstalled')
});
- return;
+ return false;
}
+
+ return theme;
+ },
+
+ preview(code) {
+ const theme = this.parseThemeCode(code);
+ if (theme) applyTheme(theme, false);
+ },
+
+ install(code) {
+ const theme = this.parseThemeCode(code);
+ if (!theme) return;
const themes = this.$store.state.device.themes.concat(theme);
this.$store.commit('device/set', {
key: 'themes', value: themes
diff --git a/src/client/router.ts b/src/client/router.ts
index 83445fea7e..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') },
@@ -58,6 +59,7 @@ export const router = new VueRouter({
{ path: '/notes/:note', name: 'note', component: page('note') },
{ path: '/tags/:tag', component: page('tag') },
{ path: '/auth/:token', component: page('auth') },
+ { path: '/miauth/:session', component: page('miauth') },
{ path: '/authorize-follow', component: page('follow') },
{ path: '/share', component: page('share') },
{ path: '*', component: page('not-found') }
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);
}