diff options
Diffstat (limited to 'src/client')
133 files changed, 913 insertions, 2111 deletions
diff --git a/src/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue index 152b900429..80086e3861 100644 --- a/src/client/app/auth/views/form.vue +++ b/src/client/app/auth/views/form.vue @@ -2,7 +2,7 @@ <div class="form"> <header> <h1><i>{{ app.name }}</i>があなたのアカウントにアクセスすることを<b>許可</b>しますか?</h1> - <img :src="`${app.iconUrl}?thumbnail&size=64`"/> + <img :src="app.iconUrl"/> </header> <div class="app"> <section> diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts index 2e58649ac2..c93609bc59 100644 --- a/src/client/app/common/scripts/compose-notification.ts +++ b/src/client/app/common/scripts/compose-notification.ts @@ -1,6 +1,6 @@ -import getNoteSummary from '../../../../renderers/get-note-summary'; -import getReactionEmoji from '../../../../renderers/get-reaction-emoji'; -import getUserName from '../../../../renderers/get-user-name'; +import getNoteSummary from '../../../../misc/get-note-summary'; +import getReactionEmoji from '../../../../misc/get-reaction-emoji'; +import getUserName from '../../../../misc/get-user-name'; type Notification = { title: string; @@ -17,21 +17,21 @@ export default function(type, data): Notification { return { title: 'ファイルがアップロードされました', body: data.name, - icon: data.url + '?thumbnail&size=64' + icon: data.url }; case 'unread_messaging_message': return { title: `${getUserName(data.user)}さんからメッセージ:`, body: data.text, // TODO: getMessagingMessageSummary(data), - icon: data.user.avatarUrl + '?thumbnail&size=64' + icon: data.user.avatarUrl }; case 'reversi_invited': return { title: '対局への招待があります', body: `${getUserName(data.parent)}さんから`, - icon: data.parent.avatarUrl + '?thumbnail&size=64' + icon: data.parent.avatarUrl }; case 'notification': @@ -40,28 +40,28 @@ export default function(type, data): Notification { return { title: `${getUserName(data.user)}さんから:`, body: getNoteSummary(data), - icon: data.user.avatarUrl + '?thumbnail&size=64' + icon: data.user.avatarUrl }; case 'reply': return { title: `${getUserName(data.user)}さんから返信:`, body: getNoteSummary(data), - icon: data.user.avatarUrl + '?thumbnail&size=64' + icon: data.user.avatarUrl }; case 'quote': return { title: `${getUserName(data.user)}さんが引用:`, body: getNoteSummary(data), - icon: data.user.avatarUrl + '?thumbnail&size=64' + icon: data.user.avatarUrl }; case 'reaction': return { title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`, body: getNoteSummary(data.note), - icon: data.user.avatarUrl + '?thumbnail&size=64' + icon: data.user.avatarUrl }; default: diff --git a/src/client/app/common/scripts/streaming/reversi-game.ts b/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts index 5638b3013f..e6b02fcfdb 100644 --- a/src/client/app/common/scripts/streaming/reversi-game.ts +++ b/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts @@ -1,9 +1,9 @@ -import Stream from './stream'; -import MiOS from '../../../mios'; +import Stream from '../../stream'; +import MiOS from '../../../../../mios'; export class ReversiGameStream extends Stream { constructor(os: MiOS, me, game) { - super(os, 'reversi-game', { + super(os, 'games/reversi-game', { i: me ? me.token : null, game: game.id }); diff --git a/src/client/app/common/scripts/streaming/reversi.ts b/src/client/app/common/scripts/streaming/games/reversi/reversi.ts index 2e4395f0f1..1f4fd8c63e 100644 --- a/src/client/app/common/scripts/streaming/reversi.ts +++ b/src/client/app/common/scripts/streaming/games/reversi/reversi.ts @@ -1,10 +1,10 @@ -import StreamManager from './stream-manager'; -import Stream from './stream'; -import MiOS from '../../../mios'; +import StreamManager from '../../stream-manager'; +import Stream from '../../stream'; +import MiOS from '../../../../../mios'; export class ReversiStream extends Stream { constructor(os: MiOS, me) { - super(os, 'reversi', { + super(os, 'games/reversi', { i: me.token }); } diff --git a/src/client/app/common/scripts/streaming/hybrid-timeline.ts b/src/client/app/common/scripts/streaming/hybrid-timeline.ts new file mode 100644 index 0000000000..cd290797c4 --- /dev/null +++ b/src/client/app/common/scripts/streaming/hybrid-timeline.ts @@ -0,0 +1,34 @@ +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../../mios'; + +/** + * Hybrid timeline stream connection + */ +export class HybridTimelineStream extends Stream { + constructor(os: MiOS, me) { + super(os, 'hybrid-timeline', { + i: me.token + }); + } +} + +export class HybridTimelineStreamManager extends StreamManager<HybridTimelineStream> { + private me; + private os: MiOS; + + constructor(os: MiOS, me) { + super(); + + this.me = me; + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new HybridTimelineStream(this.os, this.me); + } + + return this.connection; + } +} diff --git a/src/client/app/common/views/components/analog-clock.vue b/src/client/app/common/views/components/analog-clock.vue index 53fb2a8dad..43ae2ca933 100644 --- a/src/client/app/common/views/components/analog-clock.vue +++ b/src/client/app/common/views/components/analog-clock.vue @@ -39,13 +39,17 @@ export default Vue.extend({ dark: { type: Boolean, default: false + }, + smooth: { + type: Boolean, + default: false } }, data() { return { now: new Date(), - clock: null, + enabled: true, graduationsPadding: 0.5, handsPadding: 1, @@ -74,6 +78,9 @@ export default Vue.extend({ return themeColor; }, + ms(): number { + return this.now.getMilliseconds() * this.smooth; + } s(): number { return this.now.getSeconds(); }, @@ -85,13 +92,13 @@ export default Vue.extend({ }, hAngle(): number { - return Math.PI * (this.h % 12 + this.m / 60) / 6; + return Math.PI * (this.h % 12 + (this.m + (this.s + this.ms / 1000) / 60) / 60) / 6; }, mAngle(): number { - return Math.PI * (this.m + this.s / 60) / 30; + return Math.PI * (this.m + (this.s + this.ms / 1000) / 60) / 30; }, sAngle(): number { - return Math.PI * this.s / 30; + return Math.PI * (this.s + this.ms / 1000) / 30; }, graduations(): any { @@ -106,11 +113,17 @@ export default Vue.extend({ }, mounted() { - this.clock = setInterval(this.tick, 1000); + const update = () => { + if (this.enabled) { + this.tick(); + requestAnimationFrame(update); + } + }; + update(); }, beforeDestroy() { - clearInterval(this.clock); + this.enabled = false; }, methods: { diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue index 84173d20b5..cd6066877c 100644 --- a/src/client/app/common/views/components/autocomplete.vue +++ b/src/client/app/common/views/components/autocomplete.vue @@ -2,11 +2,16 @@ <div class="mk-autocomplete" @contextmenu.prevent="() => {}"> <ol class="users" ref="suggests" v-if="users.length > 0"> <li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1"> - <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/> + <img class="avatar" :src="user.avatarUrl" alt=""/> <span class="name">{{ user | userName }}</span> <span class="username">@{{ user | acct }}</span> </li> </ol> + <ol class="hashtags" ref="suggests" v-if="hashtags.length > 0"> + <li v-for="hashtag in hashtags" @click="complete(type, hashtag)" @keydown="onKeydown" tabindex="-1"> + <span class="name">{{ hashtag }}</span> + </li> + </ol> <ol class="emojis" ref="suggests" v-if="emojis.length > 0"> <li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1"> <span class="emoji">{{ emoji.emoji }}</span> @@ -48,33 +53,33 @@ emjdb.sort((a, b) => a.name.length - b.name.length); export default Vue.extend({ props: ['type', 'q', 'textarea', 'complete', 'close', 'x', 'y'], + data() { return { fetching: true, users: [], + hashtags: [], emojis: [], select: -1, emojilib } }, + computed: { items(): HTMLCollection { return (this.$refs.suggests as Element).children; } }, + updated() { //#region 位置調整 - const margin = 32; - - if (this.x + this.$el.offsetWidth > window.innerWidth - margin) { - this.$el.style.left = (this.x - this.$el.offsetWidth) + 'px'; - this.$el.style.marginLeft = '-16px'; + if (this.x + this.$el.offsetWidth > window.innerWidth) { + this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px'; } else { this.$el.style.left = this.x + 'px'; - this.$el.style.marginLeft = '0'; } - if (this.y + this.$el.offsetHeight > window.innerHeight - margin) { + if (this.y + this.$el.offsetHeight > window.innerHeight) { this.$el.style.top = (this.y - this.$el.offsetHeight) + 'px'; this.$el.style.marginTop = '0'; } else { @@ -83,6 +88,7 @@ export default Vue.extend({ } //#endregion }, + mounted() { this.textarea.addEventListener('keydown', this.onKeydown); @@ -100,6 +106,7 @@ export default Vue.extend({ }); }); }, + beforeDestroy() { this.textarea.removeEventListener('keydown', this.onKeydown); @@ -107,6 +114,7 @@ export default Vue.extend({ el.removeEventListener('mousedown', this.onMousedown); }); }, + methods: { exec() { this.select = -1; @@ -117,7 +125,8 @@ export default Vue.extend({ } if (this.type == 'user') { - const cache = sessionStorage.getItem(this.q); + const cacheKey = 'autocomplete:user:' + this.q; + const cache = sessionStorage.getItem(cacheKey); if (cache) { const users = JSON.parse(cache); this.users = users; @@ -131,9 +140,33 @@ export default Vue.extend({ this.fetching = false; // キャッシュ - sessionStorage.setItem(this.q, JSON.stringify(users)); + sessionStorage.setItem(cacheKey, JSON.stringify(users)); }); } + } else if (this.type == 'hashtag') { + if (this.q == null || this.q == '') { + this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]'); + this.fetching = false; + } else { + const cacheKey = 'autocomplete:hashtag:' + this.q; + const cache = sessionStorage.getItem(cacheKey); + if (cache) { + const hashtags = JSON.parse(cache); + this.hashtags = hashtags; + this.fetching = false; + } else { + (this as any).api('hashtags/search', { + query: this.q, + limit: 30 + }).then(hashtags => { + this.hashtags = hashtags; + this.fetching = false; + + // キャッシュ + sessionStorage.setItem(cacheKey, JSON.stringify(hashtags)); + }); + } + } } else if (this.type == 'emoji') { const matched = []; emjdb.some(x => { @@ -228,12 +261,13 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-autocomplete +root(isDark) position fixed z-index 65535 + max-width 100% margin-top calc(1em + 8px) overflow hidden - background #fff + background isDark ? #313543 : #fff border solid 1px rgba(#000, 0.1) border-radius 4px transition top 0.1s ease, left 0.1s ease @@ -248,7 +282,8 @@ export default Vue.extend({ list-style none > li - display block + display flex + align-items center padding 4px 12px white-space nowrap overflow hidden @@ -259,7 +294,13 @@ export default Vue.extend({ &, * user-select none + * + overflow hidden + text-overflow ellipsis + &:hover + background isDark ? rgba(#fff, 0.1) : rgba(#000, 0.1) + &[data-selected='true'] background $theme-color @@ -275,7 +316,6 @@ export default Vue.extend({ > .users > li .avatar - vertical-align middle min-width 28px min-height 28px max-width 28px @@ -285,10 +325,15 @@ export default Vue.extend({ .name margin 0 8px 0 0 - color rgba(#000, 0.8) + color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8) .username - color rgba(#000, 0.3) + color isDark ? rgba(#fff, 0.3) : rgba(#000, 0.3) + + > .hashtags > li + + .name + color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8) > .emojis > li @@ -298,10 +343,15 @@ export default Vue.extend({ width 24px .name - color rgba(#000, 0.8) + color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8) .alias margin 0 0 0 8px - color rgba(#000, 0.3) + color isDark ? rgba(#fff, 0.3) : rgba(#000, 0.3) + +.mk-autocomplete[data-darkmode] + root(true) +.mk-autocomplete:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue index a65b62882f..a924b62e64 100644 --- a/src/client/app/common/views/components/avatar.vue +++ b/src/client/app/common/views/components/avatar.vue @@ -31,7 +31,7 @@ export default Vue.extend({ : this.user.avatarColor && this.user.avatarColor.length == 3 ? `rgb(${ this.user.avatarColor.join(',') })` : null, - backgroundImage: this.lightmode ? null : `url(${ this.user.avatarUrl }?thumbnail)`, + backgroundImage: this.lightmode ? null : `url(${ this.user.avatarUrl })`, borderRadius: this.$store.state.settings.circleIcons ? '100%' : null }; } diff --git a/src/client/app/common/views/components/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue index a2a6fd0dc1..303070ffd8 100644 --- a/src/client/app/common/views/components/reversi.game.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.game.vue @@ -26,8 +26,8 @@ :class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }" @click="set(i)" :title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`"> - <img v-if="stone === true" :src="`${blackUser.avatarUrl}?thumbnail&size=128`" alt=""> - <img v-if="stone === false" :src="`${whiteUser.avatarUrl}?thumbnail&size=128`" alt=""> + <img v-if="stone === true" :src="blackUser.avatarUrl" alt=""> + <img v-if="stone === false" :src="whiteUser.avatarUrl" alt=""> </div> </div> <div class="labels-y" v-if="this.$store.state.settings.reversiBoardLabels"> @@ -58,8 +58,8 @@ <script lang="ts"> import Vue from 'vue'; import * as CRC32 from 'crc-32'; -import Reversi, { Color } from '../../../../../reversi/core'; -import { url } from '../../../config'; +import Reversi, { Color } from '../../../../../../../games/reversi/core'; +import { url } from '../../../../../config'; export default Vue.extend({ props: ['initGame', 'connection'], diff --git a/src/client/app/common/views/components/reversi.gameroom.vue b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue index 7ce0112451..4969a9347e 100644 --- a/src/client/app/common/views/components/reversi.gameroom.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue @@ -9,7 +9,7 @@ import Vue from 'vue'; import XGame from './reversi.game.vue'; import XRoom from './reversi.room.vue'; -import { ReversiGameStream } from '../../scripts/streaming/reversi-game'; +import { ReversiGameStream } from '../../../../scripts/streaming/games/reversi/reversi-game'; export default Vue.extend({ components: { diff --git a/src/client/app/common/views/components/reversi.room.vue b/src/client/app/common/views/components/games/reversi/reversi.room.vue index 5074845758..841f6b366a 100644 --- a/src/client/app/common/views/components/reversi.room.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.room.vue @@ -94,7 +94,7 @@ <script lang="ts"> import Vue from 'vue'; -import * as maps from '../../../../../reversi/maps'; +import * as maps from '../../../../../../../games/reversi/maps'; export default Vue.extend({ props: ['game', 'connection'], @@ -112,7 +112,7 @@ export default Vue.extend({ computed: { mapCategories(): string[] { - const categories = Object.entries(maps).map(x => x[1].category); + const categories = Object.values(maps).map(x => x.category); return categories.filter((item, pos) => categories.indexOf(item) == pos); }, isAccepted(): boolean { @@ -179,8 +179,8 @@ export default Vue.extend({ if (this.game.settings.map == null) { this.mapName = null; } else { - const foundMap = Object.entries(maps).find(x => x[1].data.join('') == this.game.settings.map.join('')); - this.mapName = foundMap ? foundMap[1].name : '-Custom-'; + const found = Object.values(maps).find(x => x.data.join('') == this.game.settings.map.join('')); + this.mapName = found ? found.name : '-Custom-'; } }, @@ -206,7 +206,7 @@ export default Vue.extend({ if (v == null) { this.game.settings.map = null; } else { - this.game.settings.map = Object.entries(maps).find(x => x[1].name == v)[1].data; + this.game.settings.map = Object.values(maps).find(x => x.name == v).data; } this.$forceUpdate(); this.updateSettings(); @@ -217,9 +217,9 @@ export default Vue.extend({ const y = Math.floor(pos / this.game.settings.map[0].length); const newPixel = pixel == ' ' ? '-' : - pixel == '-' ? 'b' : - pixel == 'b' ? 'w' : - ' '; + pixel == '-' ? 'b' : + pixel == 'b' ? 'w' : + ' '; const line = this.game.settings.map[y].split(''); line[x] = newPixel; this.$set(this.game.settings.map, y, line.join('')); diff --git a/src/client/app/common/views/components/reversi.vue b/src/client/app/common/views/components/games/reversi/reversi.vue index 61705163ac..6e64710823 100644 --- a/src/client/app/common/views/components/reversi.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.vue @@ -99,18 +99,18 @@ export default Vue.extend({ this.connection.on('matched', this.onMatched); this.connection.on('invited', this.onInvited); - (this as any).api('reversi/games', { + (this as any).api('games/reversi/games', { my: true }).then(games => { this.myGames = games; }); - (this as any).api('reversi/games').then(games => { + (this as any).api('games/reversi/games').then(games => { this.games = games; this.gamesFetching = false; }); - (this as any).api('reversi/invitations').then(invitations => { + (this as any).api('games/reversi/invitations').then(invitations => { this.invitations = this.invitations.concat(invitations); }); @@ -132,7 +132,7 @@ export default Vue.extend({ }, methods: { go(game) { - (this as any).api('reversi/games/show', { + (this as any).api('games/reversi/games/show', { gameId: game.id }).then(game => { this.matching = null; @@ -146,7 +146,7 @@ export default Vue.extend({ (this as any).api('users/show', { username }).then(user => { - (this as any).api('reversi/match', { + (this as any).api('games/reversi/match', { userId: user.id }).then(res => { if (res == null) { @@ -160,10 +160,10 @@ export default Vue.extend({ }, cancel() { this.matching = null; - (this as any).api('reversi/match/cancel'); + (this as any).api('games/reversi/match/cancel'); }, accept(invitation) { - (this as any).api('reversi/match', { + (this as any).api('games/reversi/match', { userId: invitation.parent.id }).then(game => { if (game) { diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts index e2cba2e53b..c18a1c3b68 100644 --- a/src/client/app/common/views/components/index.ts +++ b/src/client/app/common/views/components/index.ts @@ -27,7 +27,7 @@ import urlPreview from './url-preview.vue'; import twitterSetting from './twitter-setting.vue'; import fileTypeIcon from './file-type-icon.vue'; import Switch from './switch.vue'; -import Reversi from './reversi.vue'; +import Reversi from './games/reversi/reversi.vue'; import welcomeTimeline from './welcome-timeline.vue'; import uiInput from './ui/input.vue'; import uiButton from './ui/button.vue'; diff --git a/src/client/app/common/views/components/media-list.vue b/src/client/app/common/views/components/media-list.vue index 2f8a1943ad..cdfc2c8d3c 100644 --- a/src/client/app/common/views/components/media-list.vue +++ b/src/client/app/common/views/components/media-list.vue @@ -46,33 +46,45 @@ export default Vue.extend({ display grid grid-gap 4px + > * + overflow hidden + border-radius 4px + &[data-count="1"] grid-template-rows 1fr + &[data-count="2"] grid-template-columns 1fr 1fr grid-template-rows 1fr + &[data-count="3"] grid-template-columns 1fr 0.5fr grid-template-rows 1fr 1fr - :nth-child(1) + + > *:nth-child(1) grid-row 1 / 3 - :nth-child(3) + + > *:nth-child(3) grid-column 2 / 3 grid-row 2 / 3 + &[data-count="4"] grid-template-columns 1fr 1fr grid-template-rows 1fr 1fr - :nth-child(1) + > *:nth-child(1) grid-column 1 / 2 grid-row 1 / 2 - :nth-child(2) + + > *:nth-child(2) grid-column 2 / 3 grid-row 1 / 2 - :nth-child(3) + + > *:nth-child(3) grid-column 1 / 2 grid-row 2 / 3 - :nth-child(4) + + > *:nth-child(4) grid-column 2 / 3 grid-row 2 / 3 diff --git a/src/client/app/common/views/components/messaging-room.form.vue b/src/client/app/common/views/components/messaging-room.form.vue index 050906cf44..b6ca902660 100644 --- a/src/client/app/common/views/components/messaging-room.form.vue +++ b/src/client/app/common/views/components/messaging-room.form.vue @@ -119,7 +119,7 @@ export default Vue.extend({ }, onKeypress(e) { - if ((e.which == 10 || e.which == 13) && e.ctrlKey) { + if ((e.which == 10 || e.which == 13) && e.ctrlKey && this.canSend) { this.send(); } }, diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue index f33173da6f..65231aed17 100644 --- a/src/client/app/common/views/components/messaging-room.message.vue +++ b/src/client/app/common/views/components/messaging-room.message.vue @@ -3,10 +3,9 @@ <mk-avatar class="avatar" :user="message.user" target="_blank"/> <div class="content"> <div class="balloon" :data-no-text="message.text == null"> - <p class="read" v-if="isMe && message.isRead">%i18n:@is-read%</p> - <button class="delete-button" v-if="isMe" title="%i18n:common.delete%"> + <!-- <button class="delete-button" v-if="isMe" title="%i18n:common.delete%"> <img src="/assets/desktop/messaging/delete.png" alt="Delete"/> - </button> + </button> --> <div class="content" v-if="!message.isDeleted"> <misskey-flavored-markdown class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/> <div class="file" v-if="message.file"> @@ -23,6 +22,7 @@ <div></div> <mk-url-preview v-for="url in urls" :url="url" :key="url"/> <footer> + <span class="read" v-if="isMe && message.isRead">%i18n:@is-read%</span> <mk-time :time="message.createdAt"/> <template v-if="message.is_edited">%fa:pencil-alt%</template> </footer> @@ -120,17 +120,6 @@ root(isDark) height 16px cursor pointer - > .read - user-select none - display block - position absolute - z-index 1 - bottom -4px - left -12px - margin 0 - color isDark ? rgba(#fff, 0.5) : rgba(#000, 0.5) - font-size 11px - > .content > .is-deleted @@ -258,6 +247,12 @@ root(isDark) > footer text-align right + > .read + user-select none + margin 0 4px 0 0 + color isDark ? rgba(#fff, 0.5) : rgba(#000, 0.5) + font-size 11px + &[data-is-deleted] > .baloon opacity 0.5 diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue index 2ddec29984..6abfc92dca 100644 --- a/src/client/app/common/views/components/messaging.vue +++ b/src/client/app/common/views/components/messaging.vue @@ -51,7 +51,7 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; +import getAcct from '../../../../../misc/acct/render'; export default Vue.extend({ props: { diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.ts b/src/client/app/common/views/components/misskey-flavored-markdown.ts index c321c76104..c93e09fb5f 100644 --- a/src/client/app/common/views/components/misskey-flavored-markdown.ts +++ b/src/client/app/common/views/components/misskey-flavored-markdown.ts @@ -1,7 +1,7 @@ import Vue from 'vue'; import * as emojilib from 'emojilib'; import parse from '../../../../../mfm/parse'; -import getAcct from '../../../../../acct/render'; +import getAcct from '../../../../../misc/acct/render'; import { url } from '../../../config'; import MkUrl from './url.vue'; import MkGoogle from './google.vue'; diff --git a/src/client/app/common/views/components/nav.vue b/src/client/app/common/views/components/nav.vue index cd1f99288a..e25dbc78ca 100644 --- a/src/client/app/common/views/components/nav.vue +++ b/src/client/app/common/views/components/nav.vue @@ -2,9 +2,9 @@ <span class="mk-nav"> <a :href="aboutUrl">%i18n:@about%</a> <i>・</i> - <a href="https://github.com/syuilo/misskey">%i18n:@repository%</a> + <a :href="repositoryUrl">%i18n:@repository%</a> <i>・</i> - <a href="https://github.com/syuilo/misskey/issues/new" target="_blank">%i18n:@feedback%</a> + <a :href="feedbackUrl" target="_blank">%i18n:@feedback%</a> <i>・</i> <a :href="devUrl">%i18n:@develop%</a> <i>・</i> @@ -14,7 +14,7 @@ <script lang="ts"> import Vue from 'vue'; -import { docsUrl, statsUrl, statusUrl, devUrl, lang } from '../../../config'; +import { docsUrl, statsUrl, statusUrl, devUrl, repositoryUrl, feedbackUrl, lang } from '../../../config'; export default Vue.extend({ data() { @@ -22,7 +22,9 @@ export default Vue.extend({ aboutUrl: `${docsUrl}/${lang}/about`, statsUrl, statusUrl, - devUrl + devUrl, + repositoryUrl: repositoryUrl || `https://github.com/syuilo/misskey`, + feedbackUrl: feedbackUrl || `https://github.com/syuilo/misskey/issues/new` } } }); diff --git a/src/client/app/common/views/components/note-header.vue b/src/client/app/common/views/components/note-header.vue index 25a3339264..9bba6990db 100644 --- a/src/client/app/common/views/components/note-header.vue +++ b/src/client/app/common/views/components/note-header.vue @@ -2,6 +2,7 @@ <header class="bvonvjxbwzaiskogyhbwgyxvcgserpmu"> <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/> <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link> + <span class="is-verified" v-if="note.user.isVerified" title="%i18n:common.verified-user%">%fa:bookmark%</span> <span class="is-admin" v-if="note.user.isAdmin">admin</span> <span class="is-bot" v-if="note.user.isBot">bot</span> <span class="is-cat" v-if="note.user.isCat">cat</span> @@ -69,6 +70,10 @@ root(isDark) &:hover text-decoration underline + > .is-verified + margin-right 8px + color #4dabf7 + > .is-admin > .is-bot > .is-cat diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue index ed7aedb58e..5a149cc4d1 100644 --- a/src/client/app/common/views/components/reaction-picker.vue +++ b/src/client/app/common/views/components/reaction-picker.vue @@ -183,7 +183,7 @@ root(isDark) border-right solid $balloon-size transparent border-bottom solid $balloon-size $bgcolor - &.compact + &.big > div width 280px diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue index 62373a59ec..45a183e144 100644 --- a/src/client/app/common/views/components/signup.vue +++ b/src/client/app/common/views/components/signup.vue @@ -29,11 +29,7 @@ <p slot="text" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@password-not-matched%</p> </div> </ui-input> - <div class="g-recaptcha" :data-sitekey="recaptchaSitekey" style="margin: 16px 0;"></div> - <label class="agree-tou" style="display: block; margin: 16px 0;"> - <input name="agree-tou" type="checkbox" required/> - <p><a :href="touUrl" target="_blank">利用規約</a>に同意する</p> - </label> + <div v-if="recaptchaSitekey != null" class="g-recaptcha" :data-sitekey="recaptchaSitekey" style="margin: 16px 0;"></div> <ui-button type="submit">%i18n:@create%</ui-button> </form> </template> @@ -41,7 +37,7 @@ <script lang="ts"> import Vue from 'vue'; const getPasswordStrength = require('syuilo-password-strength'); -import { host, url, docsUrl, lang, recaptchaSitekey } from '../../../config'; +import { host, url, recaptchaSitekey } from '../../../config'; export default Vue.extend({ data() { @@ -51,7 +47,6 @@ export default Vue.extend({ password: '', retypedPassword: '', url, - touUrl: `${docsUrl}/${lang}/tou`, recaptchaSitekey, usernameState: null, passwordStrength: '', @@ -115,7 +110,7 @@ export default Vue.extend({ (this as any).api('signup', { username: this.username, password: this.password, - 'g-recaptcha-response': (window as any).grecaptcha.getResponse() + 'g-recaptcha-response': recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null }).then(() => { (this as any).api('signin', { username: this.username, @@ -126,15 +121,19 @@ export default Vue.extend({ }).catch(() => { alert('%i18n:@some-error%'); - (window as any).grecaptcha.reset(); + if (recaptchaSitekey != null) { + (window as any).grecaptcha.reset(); + } }); } }, mounted() { - const head = document.getElementsByTagName('head')[0]; - const script = document.createElement('script'); - script.setAttribute('src', 'https://www.google.com/recaptcha/api.js'); - head.appendChild(script); + if (recaptchaSitekey != null) { + const head = document.getElementsByTagName('head')[0]; + const script = document.createElement('script'); + script.setAttribute('src', 'https://www.google.com/recaptcha/api.js'); + head.appendChild(script); + } } }); </script> @@ -144,22 +143,4 @@ export default Vue.extend({ .mk-signup min-width 302px - - .agree-tou - padding 4px - border-radius 4px - - &:hover - background #f4f4f4 - - &:active - background #eee - - &, * - cursor pointer - - p - display inline - color #555 - </style> diff --git a/src/client/app/common/views/directives/autocomplete.ts b/src/client/app/common/views/directives/autocomplete.ts index 94635d301a..10c37a06e7 100644 --- a/src/client/app/common/views/directives/autocomplete.ts +++ b/src/client/app/common/views/directives/autocomplete.ts @@ -1,5 +1,6 @@ import * as getCaretCoordinates from 'textarea-caret'; import MkAutocomplete from '../components/autocomplete.vue'; +import renderAcct from '../../../../../misc/acct/render'; export default { bind(el, binding, vn) { @@ -67,15 +68,30 @@ class Autocomplete { * テキスト入力時 */ private onInput() { - const caret = this.textarea.selectionStart; - const text = this.text.substr(0, caret); + const caretPos = this.textarea.selectionStart; + const text = this.text.substr(0, caretPos); const mentionIndex = text.lastIndexOf('@'); + const hashtagIndex = text.lastIndexOf('#'); const emojiIndex = text.lastIndexOf(':'); + const start = Math.min( + mentionIndex == -1 ? Infinity : mentionIndex, + hashtagIndex == -1 ? Infinity : hashtagIndex, + emojiIndex == -1 ? Infinity : emojiIndex); + + if (start == Infinity) { + this.close(); + return; + } + + const isMention = mentionIndex == start; + const isHashtag = hashtagIndex == start; + const isEmoji = emojiIndex == start; + let opened = false; - if (mentionIndex != -1 && mentionIndex > emojiIndex) { + if (isMention) { const username = text.substr(mentionIndex + 1); if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) { this.open('user', username); @@ -83,7 +99,15 @@ class Autocomplete { } } - if (emojiIndex != -1 && emojiIndex > mentionIndex) { + if (isHashtag || opened == false) { + const hashtag = text.substr(hashtagIndex + 1); + if (!hashtag.includes(' ') && !hashtag.includes('\n')) { + this.open('hashtag', hashtag); + opened = true; + } + } + + if (isEmoji || opened == false) { const emoji = text.substr(emojiIndex + 1); if (emoji != '' && emoji.match(/^[\+\-a-z0-9_]+$/)) { this.open('emoji', emoji); @@ -164,13 +188,31 @@ class Autocomplete { const trimmedBefore = before.substring(0, before.lastIndexOf('@')); const after = source.substr(caret); + const acct = renderAcct(value); + + // 挿入 + this.text = trimmedBefore + '@' + acct + ' ' + after; + + // キャレットを戻す + this.vm.$nextTick(() => { + this.textarea.focus(); + const pos = trimmedBefore.length + (acct.length + 2); + this.textarea.setSelectionRange(pos, pos); + }); + } else if (type == 'hashtag') { + const source = this.text; + + const before = source.substr(0, caret); + const trimmedBefore = before.substring(0, before.lastIndexOf('#')); + const after = source.substr(caret); + // 挿入 - this.text = trimmedBefore + '@' + value.username + ' ' + after; + this.text = trimmedBefore + '#' + value + ' ' + after; // キャレットを戻す this.vm.$nextTick(() => { this.textarea.focus(); - const pos = trimmedBefore.length + (value.username.length + 2); + const pos = trimmedBefore.length + (value.length + 2); this.textarea.setSelectionRange(pos, pos); }); } else if (type == 'emoji') { diff --git a/src/client/app/common/views/filters/user.ts b/src/client/app/common/views/filters/user.ts index c5bb39f674..ca0910fc53 100644 --- a/src/client/app/common/views/filters/user.ts +++ b/src/client/app/common/views/filters/user.ts @@ -1,6 +1,6 @@ import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; -import getUserName from '../../../../../renderers/get-user-name'; +import getAcct from '../../../../../misc/acct/render'; +import getUserName from '../../../../../misc/get-user-name'; Vue.filter('acct', user => { return getAcct(user); diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue index c8e838be85..4b8c2d3b7c 100644 --- a/src/client/app/common/views/pages/follow.vue +++ b/src/client/app/common/views/pages/follow.vue @@ -31,8 +31,8 @@ <script lang="ts"> import Vue from 'vue'; -import parseAcct from '../../../../../acct/parse'; -import getUserName from '../../../../../renderers/get-user-name'; +import parseAcct from '../../../../../misc/acct/parse'; +import getUserName from '../../../../../misc/get-user-name'; import Progress from '../../../common/scripts/loading'; export default Vue.extend({ diff --git a/src/client/app/common/views/widgets/analog-clock.vue b/src/client/app/common/views/widgets/analog-clock.vue index b1177d4ddf..0de30228b3 100644 --- a/src/client/app/common/views/widgets/analog-clock.vue +++ b/src/client/app/common/views/widgets/analog-clock.vue @@ -1,8 +1,8 @@ <template> <div class="mkw-analog-clock"> - <mk-widget-container :naked="props.naked" :show-header="false"> + <mk-widget-container :naked="!(props.design % 2)" :show-header="false"> <div class="mkw-analog-clock--body"> - <mk-analog-clock :dark="$store.state.device.darkmode"/> + <mk-analog-clock :dark="$store.state.device.darkmode" :smooth="!(props.design && ~props.design)"/> </div> </mk-widget-container> </div> @@ -13,12 +13,13 @@ import define from '../../../common/define-widget'; export default define({ name: 'analog-clock', props: () => ({ - naked: false + design: -1 }) }).extend({ methods: { func() { - this.props.naked = !this.props.naked; + if (++this.props.design > 2) + this.props.design = -1; this.save(); } } diff --git a/src/client/app/common/views/widgets/calendar.vue b/src/client/app/common/views/widgets/calendar.vue index 333b56f629..9c8f1bff68 100644 --- a/src/client/app/common/views/widgets/calendar.vue +++ b/src/client/app/common/views/widgets/calendar.vue @@ -175,6 +175,7 @@ root(isDark) > .val height 4px background $theme-color + transition width .3s cubic-bezier(0.23, 1, 0.32, 1) &:nth-child(1) > .meter > .val diff --git a/src/client/app/common/views/widgets/photo-stream.vue b/src/client/app/common/views/widgets/photo-stream.vue index ae5924bb10..3e24c58e8e 100644 --- a/src/client/app/common/views/widgets/photo-stream.vue +++ b/src/client/app/common/views/widgets/photo-stream.vue @@ -5,7 +5,7 @@ <p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> <div :class="$style.stream" v-if="!fetching && images.length > 0"> - <div v-for="image in images" :class="$style.img" :style="`background-image: url(${image.url}?thumbnail&size=256)`"></div> + <div v-for="image in images" :class="$style.img" :style="`background-image: url(${image.url})`"></div> </div> <p :class="$style.empty" v-if="!fetching && images.length == 0">%i18n:@no-photos%</p> </mk-widget-container> diff --git a/src/client/app/common/views/widgets/slideshow.vue b/src/client/app/common/views/widgets/slideshow.vue index e1c28f5115..2eede03786 100644 --- a/src/client/app/common/views/widgets/slideshow.vue +++ b/src/client/app/common/views/widgets/slideshow.vue @@ -72,7 +72,7 @@ export default define({ if (this.images.length == 0) return; const index = Math.floor(Math.random() * this.images.length); - const img = `url(${ this.images[index].url }?thumbnail&size=1024)`; + const img = `url(${ this.images[index].url })`; (this.$refs.slideB as any).style.backgroundImage = img; diff --git a/src/client/app/config.ts b/src/client/app/config.ts index c6efe26cd5..ceee0a2d62 100644 --- a/src/client/app/config.ts +++ b/src/client/app/config.ts @@ -9,6 +9,8 @@ declare const _DOCS_URL_: string; declare const _STATS_URL_: string; declare const _STATUS_URL_: string; declare const _DEV_URL_: string; +declare const _REPOSITORY_URL_: string; +declare const _FEEDBACK_URL_: string; declare const _LANG_: string; declare const _LANGS_: string; declare const _RECAPTCHA_SITEKEY_: string; @@ -32,6 +34,8 @@ export const docsUrl = _DOCS_URL_; export const statsUrl = _STATS_URL_; export const statusUrl = _STATUS_URL_; export const devUrl = _DEV_URL_; +export const repositoryUrl = _REPOSITORY_URL_; +export const feedbackUrl = _FEEDBACK_URL_; export const lang = _LANG_; export const langs = _LANGS_; export const recaptchaSitekey = _RECAPTCHA_SITEKEY_; diff --git a/src/client/app/desktop/assets/index.jpg b/src/client/app/desktop/assets/index.jpg Binary files differindex 10c412efe2..c054188159 100644 --- a/src/client/app/desktop/assets/index.jpg +++ b/src/client/app/desktop/assets/index.jpg diff --git a/src/client/app/desktop/assets/remove.png b/src/client/app/desktop/assets/remove.png Binary files differindex 8b1f4c06c9..c2e222a0fc 100644 --- a/src/client/app/desktop/assets/remove.png +++ b/src/client/app/desktop/assets/remove.png diff --git a/src/client/app/desktop/views/components/calendar.vue b/src/client/app/desktop/views/components/calendar.vue index 3b0330cf61..111699618e 100644 --- a/src/client/app/desktop/views/components/calendar.vue +++ b/src/client/app/desktop/views/components/calendar.vue @@ -35,10 +35,7 @@ import Vue from 'vue'; const eachMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; function isLeapYear(year) { - return (year % 400 == 0) ? true : - (year % 100 == 0) ? false : - (year % 4 == 0) ? true : - false; + return !(year % (year % 25 ? 4 : 16)); } export default Vue.extend({ diff --git a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue index 30e59429d2..b894f0e109 100644 --- a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue +++ b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue @@ -28,7 +28,7 @@ export default Vue.extend({ default: false }, title: { - default: '%fa:R file%%i18n:@choose-prompt%s' + default: '%fa:R file%%i18n:@choose-prompt%' } }, data() { diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue index 86addb1318..6541a8f21f 100644 --- a/src/client/app/desktop/views/components/drive.file.vue +++ b/src/client/app/desktop/views/components/drive.file.vue @@ -1,5 +1,5 @@ <template> -<div class="root file" +<div class="gvfdktuvdgwhmztnuekzkswkjygptfcv" :data-is-selected="isSelected" :data-is-contextmenu-showing="isContextmenuShowing" @click="onClick" @@ -16,7 +16,7 @@ <p>%i18n:@banner%</p> </div> <div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`"> - <img :src="`${file.url}?thumbnail&size=128`" alt="" @load="onThumbnailLoaded"/> + <img :src="file.url" alt="" @load="onThumbnailLoaded"/> </div> <p class="name"> <span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> @@ -69,6 +69,11 @@ export default Vue.extend({ action: this.rename }, { type: 'item', + text: this.file.isSensitive ? '%i18n:@contextmenu.unmark-as-sensitive%' : '%i18n:@contextmenu.mark-as-sensitive%', + icon: this.file.isSensitive ? '%fa:R eye%' : '%fa:R eye-slash%', + action: this.toggleSensitive + }, null, { + type: 'item', text: '%i18n:@contextmenu.copy-url%', icon: '%fa:link%', action: this.copyUrl @@ -149,6 +154,13 @@ export default Vue.extend({ }); }, + toggleSensitive() { + (this as any).api('drive/files/update', { + fileId: this.file.id, + isSensitive: !this.file.isSensitive + }); + }, + copyUrl() { copyToClipboard(this.file.url); (this as any).apis.dialog({ @@ -312,10 +324,10 @@ root(isDark) > .ext opacity 0.5 -.root.file[data-darkmode] +.gvfdktuvdgwhmztnuekzkswkjygptfcv[data-darkmode] root(true) -.root.file:not([data-darkmode]) +.gvfdktuvdgwhmztnuekzkswkjygptfcv:not([data-darkmode]) root(false) </style> diff --git a/src/client/app/desktop/views/components/drive.folder.vue b/src/client/app/desktop/views/components/drive.folder.vue index fc0f353f47..e8077f9e3d 100644 --- a/src/client/app/desktop/views/components/drive.folder.vue +++ b/src/client/app/desktop/views/components/drive.folder.vue @@ -1,5 +1,5 @@ <template> -<div class="root folder" +<div class="ynntpczxvnusfwdyxsfuhvcmuypqopdd" :data-is-contextmenu-showing="isContextmenuShowing" :data-draghover="draghover" @click="onClick" @@ -216,10 +216,10 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.root.folder +root(isDark) padding 8px height 64px - background lighten($theme-color, 95%) + background isDark ? rgba($theme-color, 0.2) : lighten($theme-color, 95%) border-radius 4px &, * @@ -229,10 +229,10 @@ export default Vue.extend({ pointer-events none &:hover - background lighten($theme-color, 90%) + background isDark ? rgba(lighten($theme-color, 10%), 0.2) : lighten($theme-color, 90%) &:active - background lighten($theme-color, 85%) + background isDark ? rgba(darken($theme-color, 10%), 0.2) : lighten($theme-color, 85%) &[data-is-contextmenu-showing] &[data-draghover] @@ -248,16 +248,22 @@ export default Vue.extend({ border-radius 4px &[data-draghover] - background lighten($theme-color, 90%) + background isDark ? rgba(darken($theme-color, 10%), 0.2) : lighten($theme-color, 90%) > .name margin 0 font-size 0.9em - color darken($theme-color, 30%) + color isDark ? #fff : darken($theme-color, 30%) > [data-fa] margin-right 4px margin-left 2px text-align left +.ynntpczxvnusfwdyxsfuhvcmuypqopdd[data-darkmode] + root(true) + +.ynntpczxvnusfwdyxsfuhvcmuypqopdd:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue index df141b6d6c..6bad7c78a2 100644 --- a/src/client/app/desktop/views/components/drive.vue +++ b/src/client/app/desktop/views/components/drive.vue @@ -10,7 +10,10 @@ <span class="separator" v-if="folder != null">%fa:angle-right%</span> <span class="folder current" v-if="folder != null">{{ folder.name }}</span> </div> - <input class="search" type="search" placeholder=" %i18n:@search%"/> + <!-- + TODO: #343 + <input class="search" type="search" placeholder=" %i18n:@search%"/> + --> </nav> <div class="main" :class="{ uploading: uploadings.length > 0, fetching }" ref="main" diff --git a/src/client/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue index 7ed31315f1..fdab7bc1ce 100644 --- a/src/client/app/desktop/views/components/followers-window.vue +++ b/src/client/app/desktop/views/components/followers-window.vue @@ -1,7 +1,7 @@ <template> <mk-window width="400px" height="550px" @closed="$destroy"> <span slot="header" :class="$style.header"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }} + <img :src="user.avatarUrl" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }} </span> <mk-followers :user="user"/> </mk-window> diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue index b97f21e2a3..7cca833a82 100644 --- a/src/client/app/desktop/views/components/following-window.vue +++ b/src/client/app/desktop/views/components/following-window.vue @@ -1,7 +1,7 @@ <template> <mk-window width="400px" height="550px" @closed="$destroy"> <span slot="header" :class="$style.header"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }} + <img :src="user.avatarUrl" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }} </span> <mk-following :user="user"/> </mk-window> diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue index b98a4707ec..74bb03f4ed 100644 --- a/src/client/app/desktop/views/components/media-image.vue +++ b/src/client/app/desktop/views/components/media-image.vue @@ -1,5 +1,11 @@ <template> -<a class="mk-media-image" +<div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide" @click="hide = false"> + <div> + <b>%fa:exclamation-triangle% %i18n:@sensitive%</b> + <span>%i18n:@click-to-show%</span> + </div> +</div> +<a class="lcjomzwbohoelkxsnuqjiaccdbdfiazy" v-else :href="image.url" @mousemove="onMousemove" @mouseleave="onMouseleave" @@ -21,13 +27,17 @@ export default Vue.extend({ }, raw: { default: false + }, + hide: { + type: Boolean, + default: true } }, computed: { style(): any { return { 'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent', - 'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)` + 'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url})` }; } }, @@ -56,16 +66,30 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-media-image +.lcjomzwbohoelkxsnuqjiaccdbdfiazy display block cursor zoom-in overflow hidden width 100% height 100% background-position center - border-radius 4px &:not(:hover) background-size cover +.ldwbgwstjsdgcjruamauqdrffetqudry + display flex + justify-content center + align-items center + background #111 + color #fff + + > div + display table-cell + text-align center + font-size 12px + + > b + display block + </style> diff --git a/src/client/app/desktop/views/components/media-video.vue b/src/client/app/desktop/views/components/media-video.vue index 3635941e64..6c60f2da96 100644 --- a/src/client/app/desktop/views/components/media-video.vue +++ b/src/client/app/desktop/views/components/media-video.vue @@ -1,12 +1,19 @@ <template> - <video class="mk-media-video" +<div class="uofhebxjdgksfmltszlxurtjnjjsvioh" v-if="video.isSensitive && hide" @click="hide = false"> + <div> + <b>%fa:exclamation-triangle% %i18n:@sensitive%</b> + <span>%i18n:@click-to-show%</span> + </div> +</div> +<div class="vwxdhznewyashiknzolsoihtlpicqepe" v-else> + <video class="video" :src="video.url" :title="video.name" controls @dblclick.prevent="onClick" ref="video" v-if="inlinePlayable" /> - <a class="mk-media-video-thumbnail" + <a class="thumbnail" :href="video.url" :style="imageStyle" @click.prevent="onClick" @@ -14,6 +21,7 @@ v-else> %fa:R play-circle% </a> +</div> </template> <script lang="ts"> @@ -21,11 +29,23 @@ import Vue from 'vue'; import MkMediaVideoDialog from './media-video-dialog.vue'; export default Vue.extend({ - props: ['video', 'inlinePlayable'], + props: { + video: { + type: Object, + required: true + }, + inlinePlayable: { + default: false + }, + hide: { + type: Boolean, + default: true + } + }, computed: { imageStyle(): any { return { - 'background-image': `url(${this.video.url}?thumbnail&size=512)` + 'background-image': `url(${this.video.url})` }; } }, @@ -47,22 +67,39 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-media-video - display block - width 100% - height 100% - border-radius 4px +.vwxdhznewyashiknzolsoihtlpicqepe + .video + display block + width 100% + height 100% + border-radius 4px + + .thumbnail + display flex + justify-content center + align-items center + font-size 3.5em -.mk-media-video-thumbnail + cursor zoom-in + overflow hidden + background-position center + background-size cover + width 100% + height 100% + +.uofhebxjdgksfmltszlxurtjnjjsvioh display flex justify-content center align-items center - font-size 3.5em + background #111 + color #fff + + > div + display table-cell + text-align center + font-size 12px + + > b + display block - cursor zoom-in - overflow hidden - background-position center - background-size cover - width 100% - height 100% </style> diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue index cbb58b5e99..41b421b0e7 100644 --- a/src/client/app/desktop/views/components/messaging-room-window.vue +++ b/src/client/app/desktop/views/components/messaging-room-window.vue @@ -8,7 +8,7 @@ <script lang="ts"> import Vue from 'vue'; import { url } from '../../../config'; -import getAcct from '../../../../../acct/render'; +import getAcct from '../../../../../misc/acct/render'; export default Vue.extend({ props: ['user'], diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue index b8aff2d86e..3aa52f75f0 100644 --- a/src/client/app/desktop/views/components/notes.note.vue +++ b/src/client/app/desktop/views/components/notes.note.vue @@ -56,10 +56,10 @@ <button @click="menu" ref="menuButton"> %fa:ellipsis-h% </button> - <button title="%i18n:@detail"> + <!-- <button title="%i18n:@detail"> <template v-if="!isDetailOpened">%fa:caret-down%</template> <template v-if="isDetailOpened">%fa:caret-up%</template> - </button> + </button> --> </footer> </div> </article> diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue index 1206eb7136..c959508020 100644 --- a/src/client/app/desktop/views/components/notes.vue +++ b/src/client/app/desktop/views/components/notes.vue @@ -34,7 +34,7 @@ <script lang="ts"> import Vue from 'vue'; import { url } from '../../../config'; -import getNoteSummary from '../../../../../renderers/get-note-summary'; +import getNoteSummary from '../../../../../misc/get-note-summary'; import XNote from './notes.note.vue'; diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue index 32b36994db..b291e1f54a 100644 --- a/src/client/app/desktop/views/components/notifications.vue +++ b/src/client/app/desktop/views/components/notifications.vue @@ -110,7 +110,7 @@ <script lang="ts"> import Vue from 'vue'; -import getNoteSummary from '../../../../../renderers/get-note-summary'; +import getNoteSummary from '../../../../../misc/get-note-summary'; export default Vue.extend({ data() { diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue index 3832e5b38c..bc49fe276f 100644 --- a/src/client/app/desktop/views/components/post-form.vue +++ b/src/client/app/desktop/views/components/post-form.vue @@ -10,6 +10,10 @@ <span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span> <a @click="addVisibleUser">+ユーザーを追加</a> </div> + <div class="hashtags" v-if="recentHashtags.length > 0"> + <b>%i18n:@recent-tags%:</b> + <a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" title="%@click-to-tagging%">#{{ tag }}</a> + </div> <input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)"> <textarea :class="{ with: (files.length != 0 || poll) }" ref="text" v-model="text" :disabled="posting" @@ -19,7 +23,7 @@ <div class="medias" :class="{ with: poll }" v-show="files.length != 0"> <x-draggable :list="files" :options="{ animation: 150 }"> <div v-for="file in files" :key="file.id"> - <div class="img" :style="{ backgroundImage: `url(${file.url}?thumbnail&size=64)` }" :title="file.name"></div> + <div class="img" :style="{ backgroundImage: `url(${file.url})` }" :title="file.name"></div> <img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" title="%i18n:@attach-cancel%" alt=""/> </div> </x-draggable> @@ -46,6 +50,7 @@ <script lang="ts"> import Vue from 'vue'; +import insertTextAtCursor from 'insert-text-at-cursor'; import * as XDraggable from 'vuedraggable'; import getKao from '../../../common/scripts/get-kao'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; @@ -91,7 +96,8 @@ export default Vue.extend({ visibility: 'public', visibleUsers: [], autocomplete: null, - draghover: false + draghover: false, + recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]') }; }, @@ -131,7 +137,9 @@ export default Vue.extend({ }, canPost(): boolean { - return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.renote); + return !this.posting && + (1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) && + (this.text.trim().length <= 1000); } }, @@ -183,6 +191,10 @@ export default Vue.extend({ }, methods: { + addTag(tag: string) { + insertTextAtCursor(this.$refs.text, ` #${tag} `); + }, + watch() { this.$watch('text', () => this.saveDraft()); this.$watch('poll', () => this.saveDraft()); @@ -235,7 +247,7 @@ export default Vue.extend({ }, onKeydown(e) { - if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post(); + if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post(); }, onPaste(e) { @@ -297,8 +309,8 @@ export default Vue.extend({ }, err => { alert('エラー: ' + err.message); }, { - enableHighAccuracy: true - }); + enableHighAccuracy: true + }); }, removeGeo() { @@ -370,6 +382,12 @@ export default Vue.extend({ }).then(() => { this.posting = false; }); + + if (this.text && this.text != '') { + const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag); + const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; + localStorage.setItem('hashtags', JSON.stringify(hashtags.concat(history).reduce((a, c) => a.includes(c) ? a : [...a, c], []))); + } }, saveDraft() { @@ -452,7 +470,7 @@ root(isDark) margin 0 max-width 100% min-width 100% - min-height 64px + min-height 84px &:hover & + * @@ -478,6 +496,19 @@ root(isDark) margin-right 16px color isDark ? #fff : #666 + > .hashtags + margin 0 0 8px 0 + overflow hidden + white-space nowrap + font-size 14px + + > b + color isDark ? #9baec8 : darken($theme-color, 20%) + + > * + margin-right 8px + white-space nowrap + > .medias margin 0 padding 0 diff --git a/src/client/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue index 0b3a25f389..ca29a120c8 100644 --- a/src/client/app/desktop/views/components/settings.profile.vue +++ b/src/client/app/desktop/views/components/settings.profile.vue @@ -2,7 +2,7 @@ <div class="profile"> <label class="avatar ui from group"> <p>%i18n:@avatar%</p> - <img class="avatar" :src="`${$store.state.i.avatarUrl}?thumbnail&size=64`" alt="avatar"/> + <img class="avatar" :src="$store.state.i.avatarUrl" alt="avatar"/> <button class="ui" @click="updateAvatar">%i18n:@choice-avatar%</button> </label> <label class="ui from group"> diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue index 74ab45626d..00bd7a8783 100644 --- a/src/client/app/desktop/views/components/settings.vue +++ b/src/client/app/desktop/views/components/settings.vue @@ -410,7 +410,7 @@ export default Vue.extend({ localStorage.clear(); (this as any).apis.dialog({ title: '%i18n:@cache-cleared%', - text: '%i18n:@caache-cleared-desc%' + text: '%i18n:@cache-cleared-desc%' }); }, soundTest() { diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue index 1728dad286..15e188be67 100644 --- a/src/client/app/desktop/views/components/timeline.core.vue +++ b/src/client/app/desktop/views/components/timeline.core.vue @@ -43,19 +43,21 @@ export default Vue.extend({ }, stream(): any { - return this.src == 'home' - ? (this as any).os.stream - : this.src == 'local' - ? (this as any).os.streams.localTimelineStream - : (this as any).os.streams.globalTimelineStream; + switch (this.src) { + case 'home': return (this as any).os.stream; + case 'local': return (this as any).os.streams.localTimelineStream; + case 'hybrid': return (this as any).os.streams.hybridTimelineStream; + case 'global': return (this as any).os.streams.globalTimelineStream; + } }, endpoint(): string { - return this.src == 'home' - ? 'notes/timeline' - : this.src == 'local' - ? 'notes/local-timeline' - : 'notes/global-timeline'; + switch (this.src) { + case 'home': return 'notes/timeline'; + case 'local': return 'notes/local-timeline'; + case 'hybrid': return 'notes/hybrid-timeline'; + case 'global': return 'notes/global-timeline'; + } }, canFetchMore(): boolean { diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue index 0728b78aa9..52a7753438 100644 --- a/src/client/app/desktop/views/components/timeline.vue +++ b/src/client/app/desktop/views/components/timeline.vue @@ -3,12 +3,14 @@ <header> <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span> <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span> + <span :data-active="src == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span> <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> <span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span> <button @click="chooseList" title="%i18n:@list%">%fa:list%</button> </header> <x-core v-if="src == 'home'" ref="tl" key="home" src="home"/> <x-core v-if="src == 'local'" ref="tl" key="local" src="local"/> + <x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> <x-core v-if="src == 'global'" ref="tl" key="global" src="global"/> <mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> </div> @@ -48,7 +50,7 @@ export default Vue.extend({ this.list = this.$store.state.device.tl.arg; } } else if (this.$store.state.i.followingCount == 0) { - this.src = 'local'; + this.src = 'hybrid'; } }, diff --git a/src/client/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue index 42211b57fe..f46edecb10 100644 --- a/src/client/app/desktop/views/components/ui.header.nav.vue +++ b/src/client/app/desktop/views/components/ui.header.nav.vue @@ -2,13 +2,13 @@ <div class="nav"> <ul> <template v-if="$store.getters.isSignedIn"> - <li class="home" :class="{ active: $route.name == 'index' }"> + <li class="home" :class="{ active: $route.name == 'index' }" @click="goToTop"> <router-link to="/"> %fa:home% <p>%i18n:@home%</p> </router-link> </li> - <li class="deck" :class="{ active: $route.name == 'deck' }"> + <li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop"> <router-link to="/deck"> %fa:columns% <p>%i18n:@deck% <small>(beta)</small></p> @@ -82,6 +82,14 @@ export default Vue.extend({ game() { (this as any).os.new(MkGameWindow); + }, + + goToTop(e: HTMLElement) { + if (e.classList.contains('active')) + window.scrollTo({ + top: 0, + behavior: 'smooth' + }); } } }); diff --git a/src/client/app/desktop/views/components/ui.header.search.vue b/src/client/app/desktop/views/components/ui.header.search.vue index b6149a1878..9a36e52fcc 100644 --- a/src/client/app/desktop/views/components/ui.header.search.vue +++ b/src/client/app/desktop/views/components/ui.header.search.vue @@ -29,9 +29,7 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' - -.search - +root(isDark) > [data-fa] display block position absolute @@ -60,15 +58,20 @@ export default Vue.extend({ border none border-radius 16px transition color 0.5s ease, border 0.5s ease - font-family FontAwesome, sans-serif + color isDark ? #fff : #000 &::placeholder color #9eaba8 &:hover - background rgba(#000, 0.08) + background isDark ? rgba(#fff, 0.04) : rgba(#000, 0.08) &:focus box-shadow 0 0 0 2px rgba($theme-color, 0.5) !important +.search[data-darkmode] + root(true) + +.search:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue index aa7c3ac44d..d2848eac0f 100644 --- a/src/client/app/desktop/views/components/ui.header.vue +++ b/src/client/app/desktop/views/components/ui.header.vue @@ -9,6 +9,9 @@ <div class="left"> <x-nav/> </div> + <div class="center"> + <div class="icon" @click="goToTop"></div> + </div> <div class="right"> <x-search/> <x-account v-if="$store.getters.isSignedIn"/> @@ -42,6 +45,14 @@ export default Vue.extend({ XPost, XClock, }, + methods: { + goToTop() { + window.scrollTo({ + top: 0, + behavior: 'smooth' + }); + } + }, mounted() { this.$store.commit('setUiHeaderHeight', 48); @@ -142,26 +153,24 @@ root(isDark) max-width 1300px margin 0 auto - &:before - content "" - position absolute - top 0 - left 0 - display block - width 100% - height 48px - background-image isDark ? url('/assets/desktop/header-icon.dark.svg') : url('/assets/desktop/header-icon.light.svg') - background-size 24px - background-position center - background-repeat no-repeat - opacity 0.3 + > .center + margin auto + + > .icon + display block + width 48px + height 48px + background-image isDark ? url('/assets/desktop/header-icon.dark.svg') : url('/assets/desktop/header-icon.light.svg') + background-size 24px + background-position center + background-repeat no-repeat + opacity 0.3 + cursor pointer > .left - margin 0 auto 0 0 height 48px > .right - margin 0 0 0 auto height 48px > * diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue index 788881ead5..2a68150878 100644 --- a/src/client/app/desktop/views/components/user-preview.vue +++ b/src/client/app/desktop/views/components/user-preview.vue @@ -1,7 +1,7 @@ <template> <div class="mk-user-preview"> <template v-if="u != null"> - <div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl}?thumbnail&size=512)` : ''"></div> + <div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl})` : ''"></div> <mk-avatar class="avatar" :user="u" :disable-preview="true"/> <div class="title"> <router-link class="name" :to="u | userPage">{{ u | userName }}</router-link> @@ -27,7 +27,7 @@ <script lang="ts"> import Vue from 'vue'; import * as anime from 'animejs'; -import parseAcct from '../../../../../acct/parse'; +import parseAcct from '../../../../../misc/acct/parse'; export default Vue.extend({ props: { diff --git a/src/client/app/desktop/views/pages/deck/deck.column-core.vue b/src/client/app/desktop/views/pages/deck/deck.column-core.vue index 28e7f13650..7f219c0be1 100644 --- a/src/client/app/desktop/views/pages/deck/deck.column-core.vue +++ b/src/client/app/desktop/views/pages/deck/deck.column-core.vue @@ -3,6 +3,7 @@ <x-notifications-column v-else-if="column.type == 'notifications'" :column="column" :is-stacked="isStacked"/> <x-tl-column v-else-if="column.type == 'home'" :column="column" :is-stacked="isStacked"/> <x-tl-column v-else-if="column.type == 'local'" :column="column" :is-stacked="isStacked"/> +<x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked"/> <x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked"/> <x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked"/> </template> diff --git a/src/client/app/desktop/views/pages/deck/deck.notification.vue b/src/client/app/desktop/views/pages/deck/deck.notification.vue index a379adc69e..d0093ff282 100644 --- a/src/client/app/desktop/views/pages/deck/deck.notification.vue +++ b/src/client/app/desktop/views/pages/deck/deck.notification.vue @@ -81,7 +81,7 @@ <script lang="ts"> import Vue from 'vue'; -import getNoteSummary from '../../../../../../renderers/get-note-summary'; +import getNoteSummary from '../../../../../../misc/get-note-summary'; import XNote from './deck.note.vue'; export default Vue.extend({ diff --git a/src/client/app/desktop/views/pages/deck/deck.tl-column.vue b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue index ffe1da670b..231b505f5d 100644 --- a/src/client/app/desktop/views/pages/deck/deck.tl-column.vue +++ b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue @@ -3,6 +3,7 @@ <span slot="header"> <template v-if="column.type == 'home'">%fa:home%</template> <template v-if="column.type == 'local'">%fa:R comments%</template> + <template v-if="column.type == 'hybrid'">%fa:share-alt%</template> <template v-if="column.type == 'global'">%fa:globe%</template> <template v-if="column.type == 'list'">%fa:list%</template> <span>{{ name }}</span> @@ -61,6 +62,7 @@ export default Vue.extend({ switch (this.column.type) { case 'home': return '%i18n:common.deck.home%'; case 'local': return '%i18n:common.deck.local%'; + case 'hybrid': return '%i18n:common.deck.hybrid%'; case 'global': return '%i18n:common.deck.global%'; case 'list': return this.column.list.title; } diff --git a/src/client/app/desktop/views/pages/deck/deck.tl.vue b/src/client/app/desktop/views/pages/deck/deck.tl.vue index 8e05f09c5d..d402ee6a8b 100644 --- a/src/client/app/desktop/views/pages/deck/deck.tl.vue +++ b/src/client/app/desktop/views/pages/deck/deck.tl.vue @@ -41,27 +41,29 @@ export default Vue.extend({ }; }, - watch: { - mediaOnly() { - this.fetch(); - } - }, - computed: { stream(): any { - return this.src == 'home' - ? (this as any).os.stream - : this.src == 'local' - ? (this as any).os.streams.localTimelineStream - : (this as any).os.streams.globalTimelineStream; + switch (this.src) { + case 'home': return (this as any).os.stream; + case 'local': return (this as any).os.streams.localTimelineStream; + case 'hybrid': return (this as any).os.streams.hybridTimelineStream; + case 'global': return (this as any).os.streams.globalTimelineStream; + } }, endpoint(): string { - return this.src == 'home' - ? 'notes/timeline' - : this.src == 'local' - ? 'notes/local-timeline' - : 'notes/global-timeline'; + switch (this.src) { + case 'home': return 'notes/timeline'; + case 'local': return 'notes/local-timeline'; + case 'hybrid': return 'notes/hybrid-timeline'; + case 'global': return 'notes/global-timeline'; + } + }, + }, + + watch: { + mediaOnly() { + this.fetch(); } }, diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue index da4acb8cca..26b989656e 100644 --- a/src/client/app/desktop/views/pages/deck/deck.vue +++ b/src/client/app/desktop/views/pages/deck/deck.vue @@ -120,6 +120,15 @@ export default Vue.extend({ }); } }, { + icon: '%fa:share-alt%', + text: '%i18n:common.deck.hybrid%', + action: () => { + this.$store.dispatch('settings/addDeckColumn', { + id: uuid(), + type: 'hybrid' + }); + } + }, { icon: '%fa:globe%', text: '%i18n:common.deck.global%', action: () => { diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue index 06c32776c9..1ebd53cef4 100644 --- a/src/client/app/desktop/views/pages/messaging-room.vue +++ b/src/client/app/desktop/views/pages/messaging-room.vue @@ -7,8 +7,8 @@ <script lang="ts"> import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; -import parseAcct from '../../../../../acct/parse'; -import getUserName from '../../../../../renderers/get-user-name'; +import parseAcct from '../../../../../misc/acct/parse'; +import getUserName from '../../../../../misc/get-user-name'; export default Vue.extend({ data() { diff --git a/src/client/app/desktop/views/pages/reversi.vue b/src/client/app/desktop/views/pages/reversi.vue index 098fc41f1c..b484b81b5d 100644 --- a/src/client/app/desktop/views/pages/reversi.vue +++ b/src/client/app/desktop/views/pages/reversi.vue @@ -33,7 +33,7 @@ export default Vue.extend({ Progress.start(); this.fetching = true; - (this as any).api('reversi/games/show', { + (this as any).api('games/reversi/games/show', { gameId: this.$route.params.game }).then(game => { this.game = game; diff --git a/src/client/app/desktop/views/pages/search.vue b/src/client/app/desktop/views/pages/search.vue index e79ac1c739..2576c26cb1 100644 --- a/src/client/app/desktop/views/pages/search.vue +++ b/src/client/app/desktop/views/pages/search.vue @@ -6,20 +6,15 @@ <div :class="$style.loading" v-if="fetching"> <mk-ellipsis-icon/> </div> + <p :class="$style.notAvailable" v-if="!fetching && notAvailable">検索機能を利用することができません。</p> <p :class="$style.empty" v-if="!fetching && empty">%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。</p> - <mk-notes ref="timeline" :class="$style.notes" :notes="notes"> - <div slot="footer"> - <template v-if="!moreFetching">%fa:search%</template> - <template v-if="moreFetching">%fa:spinner .pulse .fw%</template> - </div> - </mk-notes> + <mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/> </mk-ui> </template> <script lang="ts"> import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; -import parse from '../../../common/scripts/parse-search-query'; const limit = 20; @@ -30,16 +25,14 @@ export default Vue.extend({ moreFetching: false, existMore: false, offset: 0, - notes: [] + empty: false, + notAvailable: false }; }, watch: { $route: 'fetch' }, computed: { - empty(): boolean { - return this.notes.length == 0; - }, q(): string { return this.$route.query.q; } @@ -66,39 +59,47 @@ export default Vue.extend({ this.fetching = true; Progress.start(); - (this as any).api('notes/search', Object.assign({ - limit: limit + 1, - offset: this.offset - }, parse(this.q))).then(notes => { - if (notes.length == limit + 1) { - notes.pop(); - this.existMore = true; - } - this.notes = notes; - this.fetching = false; - Progress.done(); - }); + (this.$refs.timeline as any).init(() => new Promise((res, rej) => { + (this as any).api('notes/search', { + limit: limit + 1, + offset: this.offset, + query: this.q + }).then(notes => { + if (notes.length == 0) this.empty = true; + if (notes.length == limit + 1) { + notes.pop(); + this.existMore = true; + } + res(notes); + this.fetching = false; + Progress.done(); + }, (e: string) => { + this.fetching = false; + Progress.done(); + if (e === 'searching not available') this.notAvailable = true; + }); + })); }, more() { - if (this.moreFetching || this.fetching || this.notes.length == 0 || !this.existMore) return; this.offset += limit; - this.moreFetching = true; - return (this as any).api('notes/search', Object.assign({ + + const promise = (this as any).api('notes/search', { limit: limit + 1, - offset: this.offset - }, parse(this.q))).then(notes => { + offset: this.offset, + query: this.q + }); + + promise.then(notes => { if (notes.length == limit + 1) { notes.pop(); } else { this.existMore = false; } - this.notes = this.notes.concat(notes); + notes.forEach(n => (this.$refs.timeline as any).append(n)); this.moreFetching = false; }); - }, - onScroll() { - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 16) this.more(); + + return promise; } } }); @@ -135,4 +136,18 @@ export default Vue.extend({ font-size 3em color #ccc + +.notAvailable + display block + margin 0 auto + padding 32px + max-width 400px + text-align center + color #999 + + > [data-fa] + display block + margin-bottom 16px + font-size 3em + color #ccc </style> diff --git a/src/client/app/desktop/views/pages/user-list.users.vue b/src/client/app/desktop/views/pages/user-list.users.vue index 517fe89750..7d9a4606a1 100644 --- a/src/client/app/desktop/views/pages/user-list.users.vue +++ b/src/client/app/desktop/views/pages/user-list.users.vue @@ -49,7 +49,8 @@ export default Vue.extend({ add() { (this as any).apis.input({ title: '%i18n:@username%', - }).then(async username => { + }).then(async (username: string) => { + if (username.startsWith('@')) username = username.slice(1); const user = await (this as any).api('users/show', { username }); diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue index 4c1b91e7a6..e4a771910a 100644 --- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue +++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue @@ -4,7 +4,7 @@ <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p> <div v-if="!fetching && users.length > 0"> <router-link v-for="user in users" :to="user | userPage" :key="user.id"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user | userName" v-user-preview="user.id"/> + <img :src="user.avatarUrl" :alt="user | userName" v-user-preview="user.id"/> </router-link> </div> <p class="empty" v-if="!fetching && users.length == 0">%i18n:@no-users%</p> diff --git a/src/client/app/desktop/views/pages/user/user.photos.vue b/src/client/app/desktop/views/pages/user/user.photos.vue index 01c4c7b31e..ce7791a96b 100644 --- a/src/client/app/desktop/views/pages/user/user.photos.vue +++ b/src/client/app/desktop/views/pages/user/user.photos.vue @@ -4,7 +4,7 @@ <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p> <div class="stream" v-if="!fetching && images.length > 0"> <div v-for="image in images" class="img" - :style="`background-image: url(${image.url}?thumbnail&size=256)`" + :style="`background-image: url(${image.url})`" ></div> </div> <p class="empty" v-if="!fetching && images.length == 0">%i18n:@no-photos%</p> diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue index fc5c900037..64a4eaa872 100644 --- a/src/client/app/desktop/views/pages/user/user.vue +++ b/src/client/app/desktop/views/pages/user/user.vue @@ -27,8 +27,8 @@ <script lang="ts"> import Vue from 'vue'; -import parseAcct from '../../../../../../acct/parse'; -import getUserName from '../../../../../../renderers/get-user-name'; +import parseAcct from '../../../../../../misc/acct/parse'; +import getUserName from '../../../../../../misc/get-user-name'; import Progress from '../../../../common/scripts/loading'; import XHeader from './user.header.vue'; import XTimeline from './user.timeline.vue'; diff --git a/src/client/app/desktop/views/widgets/notifications.vue b/src/client/app/desktop/views/widgets/notifications.vue index f75a091480..b44261d7c4 100644 --- a/src/client/app/desktop/views/widgets/notifications.vue +++ b/src/client/app/desktop/views/widgets/notifications.vue @@ -2,7 +2,7 @@ <div class="mkw-notifications"> <mk-widget-container :show-header="!props.compact"> <template slot="header">%fa:R bell%%i18n:@title%</template> - <button slot="func" title="%i18n:@settings%" @click="settings">%fa:cog%</button> + <!-- <button slot="func" title="%i18n:@settings%" @click="settings">%fa:cog%</button> --> <mk-notifications :class="$style.notifications"/> </mk-widget-container> diff --git a/src/client/app/desktop/views/widgets/post-form.vue b/src/client/app/desktop/views/widgets/post-form.vue index 3c4ade0e81..618d19efc4 100644 --- a/src/client/app/desktop/views/widgets/post-form.vue +++ b/src/client/app/desktop/views/widgets/post-form.vue @@ -45,7 +45,7 @@ export default define({ this.save(); }, onKeydown(e) { - if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post(); + if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && !this.posting && this.text) this.post(); }, post() { this.posting = true; diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue index 7b0fea3729..9702aaa90a 100644 --- a/src/client/app/desktop/views/widgets/profile.vue +++ b/src/client/app/desktop/views/widgets/profile.vue @@ -4,7 +4,7 @@ :data-melt="props.design == 2" > <div class="banner" - :style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl}?thumbnail&size=256)` : ''" + :style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''" title="%i18n:@update-banner%" @click="os.apis.updateBanner" ></div> diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts index 9a8d19adbd..565c8bf1f5 100644 --- a/src/client/app/mios.ts +++ b/src/client/app/mios.ts @@ -11,10 +11,11 @@ import { DriveStreamManager } from './common/scripts/streaming/drive'; import { ServerStatsStreamManager } from './common/scripts/streaming/server-stats'; import { NotesStatsStreamManager } from './common/scripts/streaming/notes-stats'; import { MessagingIndexStreamManager } from './common/scripts/streaming/messaging-index'; -import { ReversiStreamManager } from './common/scripts/streaming/reversi'; +import { ReversiStreamManager } from './common/scripts/streaming/games/reversi/reversi'; import Err from './common/views/components/connect-failed.vue'; import { LocalTimelineStreamManager } from './common/scripts/streaming/local-timeline'; +import { HybridTimelineStreamManager } from './common/scripts/streaming/hybrid-timeline'; import { GlobalTimelineStreamManager } from './common/scripts/streaming/global-timeline'; //#region api requests @@ -103,6 +104,7 @@ export default class MiOS extends EventEmitter { */ public streams: { localTimelineStream: LocalTimelineStreamManager; + hybridTimelineStream: HybridTimelineStreamManager; globalTimelineStream: GlobalTimelineStreamManager; driveStream: DriveStreamManager; serverStatsStream: ServerStatsStreamManager; @@ -111,6 +113,7 @@ export default class MiOS extends EventEmitter { reversiStream: ReversiStreamManager; } = { localTimelineStream: null, + hybridTimelineStream: null, globalTimelineStream: null, driveStream: null, serverStatsStream: null, @@ -230,6 +233,7 @@ export default class MiOS extends EventEmitter { // Init other stream manager this.streams.localTimelineStream = new LocalTimelineStreamManager(this, this.store.state.i); + this.streams.hybridTimelineStream = new HybridTimelineStreamManager(this, this.store.state.i); this.streams.globalTimelineStream = new GlobalTimelineStreamManager(this, this.store.state.i); this.streams.driveStream = new DriveStreamManager(this, this.store.state.i); this.streams.messagingIndexStream = new MessagingIndexStreamManager(this, this.store.state.i); diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue index 94c8ae3535..776e11ecf8 100644 --- a/src/client/app/mobile/views/components/drive.file.vue +++ b/src/client/app/mobile/views/components/drive.file.vue @@ -43,7 +43,7 @@ export default Vue.extend({ thumbnail(): any { return { 'background-color': this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent', - 'background-image': `url(${this.file.url}?thumbnail&size=128)` + 'background-image': `url(${this.file.url})` }; } }, diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue index c2f9c66e84..d9d68fa7ba 100644 --- a/src/client/app/mobile/views/components/media-image.vue +++ b/src/client/app/mobile/views/components/media-image.vue @@ -1,5 +1,11 @@ <template> -<a class="mk-media-image" :href="image.url" target="_blank" :style="style" :title="image.name"></a> +<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide" @click="hide = false"> + <div> + <b>%fa:exclamation-triangle% %i18n:@sensitive%</b> + <span>%i18n:@click-to-show%</span> + </div> +</div> +<a class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else :href="image.url" target="_blank" :style="style" :title="image.name"></a> </template> <script lang="ts"> @@ -13,11 +19,15 @@ export default Vue.extend({ }, raw: { default: false + }, + hide: { + type: Boolean, + default: true } }, computed: { style(): any { - let url = `url(${this.image.url}?thumbnail)`; + let url = `url(${this.image.url})`; if (this.$store.state.device.loadRemoteMedia || this.$store.state.device.lightmode) { url = null; @@ -35,13 +45,27 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-media-image +.gqnyydlzavusgskkfvwvjiattxdzsqlf display block overflow hidden width 100% height 100% background-position center background-size cover - border-radius 4px + +.qjewsnkgzzxlxtzncydssfbgjibiehcy + display flex + justify-content center + align-items center + background #111 + color #fff + + > div + display table-cell + text-align center + font-size 12px + + > b + display block </style> diff --git a/src/client/app/mobile/views/components/media-video.vue b/src/client/app/mobile/views/components/media-video.vue index 68cd48587a..aea7f41460 100644 --- a/src/client/app/mobile/views/components/media-video.vue +++ b/src/client/app/mobile/views/components/media-video.vue @@ -1,28 +1,43 @@ <template> - <a class="mk-media-video" - :href="video.url" - target="_blank" - :style="imageStyle" - :title="video.name"> - %fa:R play-circle% - </a> +<div class="icozogqfvdetwohsdglrbswgrejoxbdj" v-if="video.isSensitive && hide" @click="hide = false"> + <div> + <b>%fa:exclamation-triangle% %i18n:@sensitive%</b> + <span>%i18n:@click-to-show%</span> + </div> +</div> +<a class="kkjnbbplepmiyuadieoenjgutgcmtsvu" v-else + :href="video.url" + target="_blank" + :style="imageStyle" + :title="video.name"> + %fa:R play-circle% +</a> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ - props: ['video'], + props: { + video: { + type: Object, + required: true + }, + hide: { + type: Boolean, + default: true + } + }, computed: { imageStyle(): any { return { - 'background-image': `url(${this.video.url}?thumbnail&size=512)` + 'background-image': `url(${this.video.url})` }; } },}) </script> <style lang="stylus" scoped> -.mk-media-video +.kkjnbbplepmiyuadieoenjgutgcmtsvu display flex justify-content center align-items center @@ -33,4 +48,20 @@ export default Vue.extend({ background-size cover width 100% height 100% + +.icozogqfvdetwohsdglrbswgrejoxbdj + display flex + justify-content center + align-items center + background #111 + color #fff + + > div + display table-cell + text-align center + font-size 12px + + > b + display block + </style> diff --git a/src/client/app/mobile/views/components/note-card.vue b/src/client/app/mobile/views/components/note-card.vue index 89700b5e82..e8427798cd 100644 --- a/src/client/app/mobile/views/components/note-card.vue +++ b/src/client/app/mobile/views/components/note-card.vue @@ -2,7 +2,7 @@ <div class="mk-note-card"> <a :href="note | notePage"> <header> - <img :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/><h3>{{ note.user | userName }}</h3> + <img :src="note.user.avatarUrl" alt="avatar"/><h3>{{ note.user | userName }}</h3> </header> <div> {{ text }} @@ -14,7 +14,7 @@ <script lang="ts"> import Vue from 'vue'; -import summary from '../../../../../renderers/get-note-summary'; +import summary from '../../../../../misc/get-note-summary'; export default Vue.extend({ props: ['note'], diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue index 06d22c7258..01f3d76c74 100644 --- a/src/client/app/mobile/views/components/notes.vue +++ b/src/client/app/mobile/views/components/notes.vue @@ -37,7 +37,7 @@ <script lang="ts"> import Vue from 'vue'; -import getNoteSummary from '../../../../../renderers/get-note-summary'; +import getNoteSummary from '../../../../../misc/get-note-summary'; const displayLimit = 30; diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue index 5e2306932b..be2c7a60ed 100644 --- a/src/client/app/mobile/views/components/notification-preview.vue +++ b/src/client/app/mobile/views/components/notification-preview.vue @@ -66,7 +66,7 @@ <script lang="ts"> import Vue from 'vue'; -import getNoteSummary from '../../../../../renderers/get-note-summary'; +import getNoteSummary from '../../../../../misc/get-note-summary'; export default Vue.extend({ props: ['notification'], diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue index bbcae05f10..ee90c6b46b 100644 --- a/src/client/app/mobile/views/components/notification.vue +++ b/src/client/app/mobile/views/components/notification.vue @@ -81,7 +81,7 @@ <script lang="ts"> import Vue from 'vue'; -import getNoteSummary from '../../../../../renderers/get-note-summary'; +import getNoteSummary from '../../../../../misc/get-note-summary'; export default Vue.extend({ props: ['notification'], diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue index 1015a44115..4333fefb39 100644 --- a/src/client/app/mobile/views/components/post-form.vue +++ b/src/client/app/mobile/views/components/post-form.vue @@ -1,47 +1,53 @@ <template> <div class="mk-post-form"> - <header> - <button class="cancel" @click="cancel">%fa:times%</button> - <div> - <span class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</span> - <span class="geo" v-if="geo">%fa:map-marker-alt%</span> - <button class="submit" :disabled="!canPost" @click="post">{{ submitText }}</button> - </div> - </header> <div class="form"> - <mk-note-preview v-if="reply" :note="reply"/> - <mk-note-preview v-if="renote" :note="renote"/> - <div v-if="visibility == 'specified'" class="visibleUsers"> - <span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span> - <a @click="addVisibleUser">+%i18n:@add-visible-user%</a> - </div> - <input v-show="useCw" v-model="cw" placeholder="%i18n:@cw-placeholder%"> - <textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder"></textarea> - <div class="attaches" v-show="files.length != 0"> - <x-draggable class="files" :list="files" :options="{ animation: 150 }"> - <div class="file" v-for="file in files" :key="file.id"> - <div class="img" :style="`background-image: url(${file.url}?thumbnail&size=128)`" @click="detachMedia(file)"></div> - </div> - </x-draggable> + <header> + <button class="cancel" @click="cancel">%fa:times%</button> + <div> + <span class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</span> + <span class="geo" v-if="geo">%fa:map-marker-alt%</span> + <button class="submit" :disabled="!canPost" @click="post">{{ submitText }}</button> + </div> + </header> + <div class="form"> + <mk-note-preview v-if="reply" :note="reply"/> + <mk-note-preview v-if="renote" :note="renote"/> + <div v-if="visibility == 'specified'" class="visibleUsers"> + <span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span> + <a @click="addVisibleUser">+%i18n:@add-visible-user%</a> + </div> + <input v-show="useCw" v-model="cw" placeholder="%i18n:@cw-placeholder%"> + <textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder" v-autocomplete="'text'"></textarea> + <div class="attaches" v-show="files.length != 0"> + <x-draggable class="files" :list="files" :options="{ animation: 150 }"> + <div class="file" v-for="file in files" :key="file.id"> + <div class="img" :style="`background-image: url(${file.url})`" @click="detachMedia(file)"></div> + </div> + </x-draggable> + </div> + <mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false"/> + <mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> + <footer> + <button class="upload" @click="chooseFile">%fa:upload%</button> + <button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button> + <button class="kao" @click="kao">%fa:R smile%</button> + <button class="poll" @click="poll = true">%fa:chart-pie%</button> + <button class="poll" @click="useCw = !useCw">%fa:eye-slash%</button> + <button class="geo" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button> + <button class="visibility" @click="setVisibility" ref="visibilityButton">%fa:lock%</button> + </footer> + <input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/> </div> - <mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false"/> - <mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> - <footer> - <button class="upload" @click="chooseFile">%fa:upload%</button> - <button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button> - <button class="kao" @click="kao">%fa:R smile%</button> - <button class="poll" @click="poll = true">%fa:chart-pie%</button> - <button class="poll" @click="useCw = !useCw">%fa:eye-slash%</button> - <button class="geo" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button> - <button class="visibility" @click="setVisibility" ref="visibilityButton">%fa:lock%</button> - </footer> - <input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/> + </div> + <div class="hashtags" v-if="recentHashtags.length > 0"> + <a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)">#{{ tag }}</a> </div> </div> </template> <script lang="ts"> import Vue from 'vue'; +import insertTextAtCursor from 'insert-text-at-cursor'; import * as XDraggable from 'vuedraggable'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; import getKao from '../../../common/scripts/get-kao'; @@ -85,7 +91,8 @@ export default Vue.extend({ visibility: 'public', visibleUsers: [], useCw: false, - cw: null + cw: null, + recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]') }; }, @@ -125,7 +132,9 @@ export default Vue.extend({ }, canPost(): boolean { - return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.renote); + return !this.posting && + (1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) && + (this.text.trim().length <= 1000); } }, @@ -161,6 +170,10 @@ export default Vue.extend({ }, methods: { + addTag(tag: string) { + insertTextAtCursor(this.$refs.text, ` #${tag} `); + }, + focus() { (this.$refs.text as any).focus(); }, @@ -210,8 +223,8 @@ export default Vue.extend({ }, err => { alert('%i18n:@error%: ' + err.message); }, { - enableHighAccuracy: true - }); + enableHighAccuracy: true + }); }, removeGeo() { @@ -281,6 +294,12 @@ export default Vue.extend({ }).catch(err => { this.posting = false; }); + + if (this.text && this.text != '') { + const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag); + const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; + localStorage.setItem('hashtags', JSON.stringify(hashtags.concat(history).reduce((a, c) => a.includes(c) ? a : [...a, c], []))); + } }, cancel() { @@ -302,146 +321,156 @@ root(isDark) max-width 500px width calc(100% - 16px) margin 8px auto - background isDark ? #282C37 : #fff - border-radius 8px - box-shadow 0 0 2px rgba(#000, 0.1) @media (min-width 500px) margin 16px auto width calc(100% - 32px) - box-shadow 0 8px 32px rgba(#000, 0.1) + + > .form + box-shadow 0 8px 32px rgba(#000, 0.1) @media (min-width 600px) margin 32px auto - > header - z-index 1000 - height 50px - box-shadow 0 1px 0 0 isDark ? rgba(#000, 0.2) : rgba(#000, 0.1) - - > .cancel - padding 0 - width 50px - line-height 50px - font-size 24px - color isDark ? #9baec8 : #555 - - > div - position absolute - top 0 - right 0 - color #657786 + > .form + background isDark ? #282C37 : #fff + border-radius 8px + box-shadow 0 0 2px rgba(#000, 0.1) - > .text-count - line-height 50px + > header + z-index 1000 + height 50px + box-shadow 0 1px 0 0 isDark ? rgba(#000, 0.2) : rgba(#000, 0.1) - > .geo - margin 0 8px + > .cancel + padding 0 + width 50px line-height 50px + font-size 24px + color isDark ? #9baec8 : #555 - > .submit - margin 8px - padding 0 16px - line-height 34px - vertical-align bottom - color $theme-color-foreground - background $theme-color - border-radius 4px + > div + position absolute + top 0 + right 0 + color #657786 - &:disabled - opacity 0.7 + > .text-count + line-height 50px - > .form - max-width 500px - margin 0 auto + > .geo + margin 0 8px + line-height 50px - > .mk-note-preview - padding 16px + > .submit + margin 8px + padding 0 16px + line-height 34px + vertical-align bottom + color $theme-color-foreground + background $theme-color + border-radius 4px - > .visibleUsers - margin-bottom 8px - font-size 14px + &:disabled + opacity 0.7 - > span - margin-right 16px - color isDark ? #fff : #666 + > .form + max-width 500px + margin 0 auto - > input - z-index 1 + > .mk-note-preview + padding 16px - > input - > textarea - display block - padding 12px - margin 0 - width 100% - font-size 16px - color isDark ? #fff : #333 - background isDark ? #191d23 : #fff - border none - border-radius 0 - box-shadow 0 1px 0 0 isDark ? rgba(#000, 0.2) : rgba(#000, 0.1) + > .visibleUsers + margin-bottom 8px + font-size 14px - &:disabled - opacity 0.5 + > span + margin-right 16px + color isDark ? #fff : #666 - > textarea - max-width 100% - min-width 100% - min-height 80px + > input + z-index 1 - > .attaches - - > .files + > input + > textarea display block + padding 12px margin 0 - padding 4px - list-style none + width 100% + font-size 16px + color isDark ? #fff : #333 + background isDark ? #191d23 : #fff + border none + border-radius 0 + box-shadow 0 1px 0 0 isDark ? rgba(#000, 0.2) : rgba(#000, 0.1) - &:after - content "" - display block - clear both + &:disabled + opacity 0.5 - > .file + > textarea + max-width 100% + min-width 100% + min-height 80px + + > .attaches + + > .files display block - float left margin 0 - padding 0 - border solid 4px transparent + padding 4px + list-style none - > .img - width 64px - height 64px - background-size cover - background-position center center + &:after + content "" + display block + clear both - > .mk-uploader - margin 8px 0 0 0 - padding 8px + > .file + display block + float left + margin 0 + padding 0 + border solid 4px transparent - > .file - display none + > .img + width 64px + height 64px + background-size cover + background-position center center - > footer - white-space nowrap - overflow auto - -webkit-overflow-scrolling touch - overflow-scrolling touch + > .mk-uploader + margin 8px 0 0 0 + padding 8px - > * - display inline-block - padding 0 - margin 0 - width 48px - height 48px - font-size 20px - color #657786 - background transparent - outline none - border none - border-radius 0 - box-shadow none + > .file + display none + + > footer + white-space nowrap + overflow auto + -webkit-overflow-scrolling touch + overflow-scrolling touch + + > * + display inline-block + padding 0 + margin 0 + width 48px + height 48px + font-size 20px + color #657786 + background transparent + outline none + border none + border-radius 0 + box-shadow none + + > .hashtags + margin 8px + + > * + margin-right 8px .mk-post-form[data-darkmode] root(true) diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue index bb7a2f558c..5257dafd0e 100644 --- a/src/client/app/mobile/views/components/ui.nav.vue +++ b/src/client/app/mobile/views/components/ui.nav.vue @@ -10,7 +10,7 @@ <transition name="nav"> <div class="body" v-if="isOpen"> <router-link class="me" v-if="$store.getters.isSignedIn" :to="`/@${$store.state.i.username}`"> - <img class="avatar" :src="`${$store.state.i.avatarUrl}?thumbnail&size=128`" alt="avatar"/> + <img class="avatar" :src="$store.state.i.avatarUrl" alt="avatar"/> <p class="name">{{ $store.state.i | userName }}</p> </router-link> <div class="links"> diff --git a/src/client/app/mobile/views/components/user-card.vue b/src/client/app/mobile/views/components/user-card.vue index 808ee72402..7b8f2251b2 100644 --- a/src/client/app/mobile/views/components/user-card.vue +++ b/src/client/app/mobile/views/components/user-card.vue @@ -1,6 +1,6 @@ <template> <div class="mk-user-card"> - <header :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"> + <header :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"> <mk-avatar class="avatar" :user="user"/> </header> <a class="name" :href="user | userPage" target="_blank">{{ user | userName }}</a> diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue index dfb9c62142..7dc72a7c30 100644 --- a/src/client/app/mobile/views/pages/followers.vue +++ b/src/client/app/mobile/views/pages/followers.vue @@ -1,7 +1,7 @@ <template> <mk-ui> <template slot="header" v-if="!fetching"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""> + <img :src="user.avatarUrl" alt=""> {{ '%i18n:@followers-of%'.replace('{}', name) }} </template> <mk-users-list @@ -19,8 +19,8 @@ <script lang="ts"> import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; -import parseAcct from '../../../../../acct/parse'; -import getUserName from '../../../../../renderers/get-user-name'; +import parseAcct from '../../../../../misc/acct/parse'; +import getUserName from '../../../../../misc/get-user-name'; export default Vue.extend({ data() { diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue index 35461ea2fc..6895a76d53 100644 --- a/src/client/app/mobile/views/pages/following.vue +++ b/src/client/app/mobile/views/pages/following.vue @@ -1,7 +1,7 @@ <template> <mk-ui> <template slot="header" v-if="!fetching"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""> + <img :src="user.avatarUrl" alt=""> {{ '%i18n:@following-of%'.replace('{}', name) }} </template> <mk-users-list @@ -19,7 +19,7 @@ <script lang="ts"> import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; -import parseAcct from '../../../../../acct/parse'; +import parseAcct from '../../../../../misc/acct/parse'; export default Vue.extend({ data() { diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue index 364367b940..93d1364e09 100644 --- a/src/client/app/mobile/views/pages/home.timeline.vue +++ b/src/client/app/mobile/views/pages/home.timeline.vue @@ -42,19 +42,21 @@ export default Vue.extend({ }, stream(): any { - return this.src == 'home' - ? (this as any).os.stream - : this.src == 'local' - ? (this as any).os.streams.localTimelineStream - : (this as any).os.streams.globalTimelineStream; + switch (this.src) { + case 'home': return (this as any).os.stream; + case 'local': return (this as any).os.streams.localTimelineStream; + case 'hybrid': return (this as any).os.streams.hybridTimelineStream; + case 'global': return (this as any).os.streams.globalTimelineStream; + } }, endpoint(): string { - return this.src == 'home' - ? 'notes/timeline' - : this.src == 'local' - ? 'notes/local-timeline' - : 'notes/global-timeline'; + switch (this.src) { + case 'home': return 'notes/timeline'; + case 'local': return 'notes/local-timeline'; + case 'hybrid': return 'notes/hybrid-timeline'; + case 'global': return 'notes/global-timeline'; + } }, canFetchMore(): boolean { diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue index c0c2ee8ab5..2f57e422a3 100644 --- a/src/client/app/mobile/views/pages/home.vue +++ b/src/client/app/mobile/views/pages/home.vue @@ -4,6 +4,7 @@ <span> <span v-if="src == 'home'">%fa:home%%i18n:@home%</span> <span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span> + <span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span> <span v-if="src == 'global'">%fa:globe%%i18n:@global%</span> <span v-if="src == 'list'">%fa:list%{{ list.title }}</span> </span> @@ -24,6 +25,7 @@ <div> <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span> <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span> + <span :data-active="src == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span> <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> <template v-if="lists"> <span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span> @@ -35,6 +37,7 @@ <div class="tl"> <x-tl v-if="src == 'home'" ref="tl" key="home" src="home"/> <x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/> + <x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> <x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/> <mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> </div> @@ -88,7 +91,7 @@ export default Vue.extend({ this.list = this.$store.state.device.tl.arg; } } else if (this.$store.state.i.followingCount == 0) { - this.src = 'local'; + this.src = 'hybrid'; } }, diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue index 8b82b03fb9..35ae506761 100644 --- a/src/client/app/mobile/views/pages/messaging-room.vue +++ b/src/client/app/mobile/views/pages/messaging-room.vue @@ -10,7 +10,7 @@ <script lang="ts"> import Vue from 'vue'; -import parseAcct from '../../../../../acct/parse'; +import parseAcct from '../../../../../misc/acct/parse'; export default Vue.extend({ data() { diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue index 057470efd9..8dcbc5d6c5 100644 --- a/src/client/app/mobile/views/pages/messaging.vue +++ b/src/client/app/mobile/views/pages/messaging.vue @@ -7,7 +7,7 @@ <script lang="ts"> import Vue from 'vue'; -import getAcct from '../../../../../acct/render'; +import getAcct from '../../../../../misc/acct/render'; export default Vue.extend({ mounted() { diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue index 64cfa60da0..fcd930997b 100644 --- a/src/client/app/mobile/views/pages/notifications.vue +++ b/src/client/app/mobile/views/pages/notifications.vue @@ -24,7 +24,7 @@ export default Vue.extend({ const ok = window.confirm('%i18n:@read-all%'); if (!ok) return; - (this as any).api('notifications/mark_as_read_all'); + (this as any).api('notifications/mark_all_as_read'); }, onFetched() { Progress.done(); diff --git a/src/client/app/mobile/views/pages/reversi.vue b/src/client/app/mobile/views/pages/reversi.vue index e2f0db6d87..0cff1317aa 100644 --- a/src/client/app/mobile/views/pages/reversi.vue +++ b/src/client/app/mobile/views/pages/reversi.vue @@ -33,7 +33,7 @@ export default Vue.extend({ Progress.start(); this.fetching = true; - (this as any).api('reversi/games/show', { + (this as any).api('games/reversi/games/show', { gameId: this.$route.params.game }).then(game => { this.game = game; diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue index 9850fbcfb4..2559922efb 100644 --- a/src/client/app/mobile/views/pages/search.vue +++ b/src/client/app/mobile/views/pages/search.vue @@ -1,14 +1,10 @@ <template> <mk-ui> <span slot="header">%fa:search% {{ q }}</span> - <main v-if="!fetching"> - <mk-notes :class="$style.notes" :notes="notes"> - <span v-if="notes.length == 0">{{ '%i18n:@empty%'.replace('{}', q) }}</span> - <button v-if="existMore" @click="more" :disabled="fetching" slot="tail"> - <span v-if="!fetching">%i18n:@load-more%</span> - <span v-if="fetching">%i18n:common.loading%<mk-ellipsis/></span> - </button> - </mk-notes> + + <main> + <p v-if="!fetching && empty">%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。</p> + <mk-notes ref="timeline" :more="existMore ? more : null"/> </main> </mk-ui> </template> @@ -16,7 +12,6 @@ <script lang="ts"> import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; -import parse from '../../../common/scripts/parse-search-query'; const limit = 20; @@ -24,8 +19,9 @@ export default Vue.extend({ data() { return { fetching: true, + moreFetching: false, existMore: false, - notes: [], + empty: false, offset: 0 }; }, @@ -47,31 +43,43 @@ export default Vue.extend({ this.fetching = true; Progress.start(); - (this as any).api('notes/search', Object.assign({ - limit: limit + 1 - }, parse(this.q))).then(notes => { - if (notes.length == limit + 1) { - notes.pop(); - this.existMore = true; - } - this.notes = notes; - this.fetching = false; - Progress.done(); - }); + (this.$refs.timeline as any).init(() => new Promise((res, rej) => { + (this as any).api('notes/search', { + limit: limit + 1, + offset: this.offset, + query: this.q + }).then(notes => { + if (notes.length == 0) this.empty = true; + if (notes.length == limit + 1) { + notes.pop(); + this.existMore = true; + } + res(notes); + this.fetching = false; + Progress.done(); + }, rej); + })); }, more() { this.offset += limit; - (this as any).api('notes/search', Object.assign({ + + const promise = (this as any).api('notes/search', { limit: limit + 1, - offset: this.offset - }, parse(this.q))).then(notes => { + offset: this.offset, + query: this.q + }); + + promise.then(notes => { if (notes.length == limit + 1) { notes.pop(); } else { this.existMore = false; } - this.notes = this.notes.concat(notes); + notes.forEach(n => (this.$refs.timeline as any).append(n)); + this.moreFetching = false; }); + + return promise; } } }); diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue index ba9de9f8a6..2bc89b81be 100644 --- a/src/client/app/mobile/views/pages/user.vue +++ b/src/client/app/mobile/views/pages/user.vue @@ -1,6 +1,6 @@ <template> <mk-ui> - <template slot="header" v-if="!fetching"><img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">{{ user | userName }}</template> + <template slot="header" v-if="!fetching"><img :src="user.avatarUrl" alt="">{{ user | userName }}</template> <main v-if="!fetching" :data-darkmode="$store.state.device.darkmode"> <div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div> <div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div> @@ -64,7 +64,7 @@ <script lang="ts"> import Vue from 'vue'; import * as age from 's-age'; -import parseAcct from '../../../../../acct/parse'; +import parseAcct from '../../../../../misc/acct/parse'; import Progress from '../../../common/scripts/loading'; import XHome from './user/home.vue'; diff --git a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue index 6f809d889e..d5e3bef963 100644 --- a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue +++ b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue @@ -3,7 +3,7 @@ <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p> <div v-if="!fetching && users.length > 0"> <a v-for="user in users" :key="user.id" :href="user | userPage"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user | userName"/> + <img :src="user.avatarUrl" :alt="user | userName"/> </a> </div> <p class="empty" v-if="!fetching && users.length == 0">%i18n:@no-users%</p> diff --git a/src/client/app/mobile/views/pages/user/home.photos.vue b/src/client/app/mobile/views/pages/user/home.photos.vue index bfd2aa8332..73ff1d5173 100644 --- a/src/client/app/mobile/views/pages/user/home.photos.vue +++ b/src/client/app/mobile/views/pages/user/home.photos.vue @@ -4,7 +4,7 @@ <div class="stream" v-if="!fetching && images.length > 0"> <a v-for="image in images" class="img" - :style="`background-image: url(${image.media.url}?thumbnail&size=256)`" + :style="`background-image: url(${image.media.url})`" :href="image.note | notePage" ></a> </div> diff --git a/src/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue index a94f7e94b8..6ce3468c49 100644 --- a/src/client/app/mobile/views/widgets/profile.vue +++ b/src/client/app/mobile/views/widgets/profile.vue @@ -2,10 +2,10 @@ <div class="mkw-profile"> <mk-widget-container> <div :class="$style.banner" - :style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl}?thumbnail&size=256)` : ''" + :style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''" ></div> <img :class="$style.avatar" - :src="`${$store.state.i.avatarUrl}?thumbnail&size=96`" + :src="$store.state.i.avatarUrl" alt="avatar" /> <router-link :class="$style.name" :to="$store.state.i | userPage">{{ $store.state.i | userName }}</router-link> diff --git a/src/client/assets/error.jpg b/src/client/assets/error.jpg Binary files differindex 872b1a3f5d..24d92f3803 100644 --- a/src/client/assets/error.jpg +++ b/src/client/assets/error.jpg diff --git a/src/client/assets/pointer.png b/src/client/assets/pointer.png Binary files differindex c8bd07a3ae..0d03f75d2b 100644 --- a/src/client/assets/pointer.png +++ b/src/client/assets/pointer.png diff --git a/src/client/assets/reactions/angry.png b/src/client/assets/reactions/angry.png Binary files differindex d81c431a25..7e32dd6809 100644 --- a/src/client/assets/reactions/angry.png +++ b/src/client/assets/reactions/angry.png diff --git a/src/client/assets/reactions/confused.png b/src/client/assets/reactions/confused.png Binary files differindex cfaa60146f..c791854183 100644 --- a/src/client/assets/reactions/confused.png +++ b/src/client/assets/reactions/confused.png diff --git a/src/client/assets/reactions/congrats.png b/src/client/assets/reactions/congrats.png Binary files differindex 350adda322..fdea27fcb9 100644 --- a/src/client/assets/reactions/congrats.png +++ b/src/client/assets/reactions/congrats.png diff --git a/src/client/assets/reactions/hmm.png b/src/client/assets/reactions/hmm.png Binary files differindex a9a7e9ac88..725fe3898d 100644 --- a/src/client/assets/reactions/hmm.png +++ b/src/client/assets/reactions/hmm.png diff --git a/src/client/assets/reactions/laugh.png b/src/client/assets/reactions/laugh.png Binary files differindex cd2225ffe1..3b3c10a27a 100644 --- a/src/client/assets/reactions/laugh.png +++ b/src/client/assets/reactions/laugh.png diff --git a/src/client/assets/reactions/like.png b/src/client/assets/reactions/like.png Binary files differindex 9fe67c9109..526b391f96 100644 --- a/src/client/assets/reactions/like.png +++ b/src/client/assets/reactions/like.png diff --git a/src/client/assets/reactions/love.png b/src/client/assets/reactions/love.png Binary files differindex b8a7532ef0..9fe82cd070 100644 --- a/src/client/assets/reactions/love.png +++ b/src/client/assets/reactions/love.png diff --git a/src/client/assets/reactions/pudding.png b/src/client/assets/reactions/pudding.png Binary files differindex 27a6b048e8..e4d10a229d 100644 --- a/src/client/assets/reactions/pudding.png +++ b/src/client/assets/reactions/pudding.png diff --git a/src/client/assets/reactions/surprise.png b/src/client/assets/reactions/surprise.png Binary files differindex 5904cb2c6c..aa55592ded 100644 --- a/src/client/assets/reactions/surprise.png +++ b/src/client/assets/reactions/surprise.png diff --git a/src/client/docs/about.en.pug b/src/client/docs/about.en.pug deleted file mode 100644 index 893d9dd6a1..0000000000 --- a/src/client/docs/about.en.pug +++ /dev/null @@ -1,3 +0,0 @@ -h1 About Misskey - -p Misskey is a mini blog SNS. diff --git a/src/client/docs/about.ja.pug b/src/client/docs/about.ja.pug deleted file mode 100644 index fec933b0c6..0000000000 --- a/src/client/docs/about.ja.pug +++ /dev/null @@ -1,3 +0,0 @@ -h1 Misskeyについて - -p MisskeyはミニブログSNSです。 diff --git a/src/client/docs/api.ja.pug b/src/client/docs/api.ja.pug deleted file mode 100644 index 665cfdc4b8..0000000000 --- a/src/client/docs/api.ja.pug +++ /dev/null @@ -1,103 +0,0 @@ -h1 Misskey API - -p MisskeyはWeb APIを公開しており、様々な操作をプログラム上から行うことができます。 -p APIを自分のアカウントから利用する場合(自分のアカウントのみ操作したい場合)と、アプリケーションから利用する場合(不特定のアカウントを操作したい場合)とで利用手順が異なりますので、それぞれのケースについて説明します。 - -section - h2 自分の所有するアカウントからAPIにアクセスする場合 - p 「設定 > API」で、APIにアクセスするのに必要なAPIキーを取得してください。 - p APIにアクセスする際には、リクエストにAPIキーを「i」というパラメータ名で含めます。 - div.ui.info.warn: p %fa:exclamation-triangle%アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。 - p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。 - -section - h2 アプリケーションからAPIにアクセスする場合 - p - | 直接ユーザーのAPIキーをアプリケーションが扱うのは危険なので、 - | アプリケーションからAPIを利用する際には、アプリケーションとアプリケーションを利用するユーザーが結び付けられた専用のトークン(アクセストークン)をMisskeyに発行してもらい、 - | そのトークンをリクエストのパラメータに含める必要があります。 - div.ui.info: p %fa:info-circle%アクセストークンは、ユーザーが自分のアカウントにあなたのアプリケーションがアクセスすることを許可した場合のみ発行されます - - p それでは、アクセストークンを取得するまでの流れを説明します。 - - section - h3 1.アプリケーションを登録する - p まず、あなたのアプリケーションやWebサービス(以後、あなたのアプリと呼びます)をMisskeyに登録します。 - p - a(href=common.config.dev_url, target="_blank") デベロッパーセンター - | にアクセスし、「アプリ > アプリ作成」に進みます。 - | フォームに必要事項を記入し、アプリを作成してください。フォームの記入欄の説明は以下の通りです: - - table - thead - tr - th 名前 - th 説明 - tbody - tr - td アプリケーション名 - td あなたのアプリの名称。 - tr - td アプリの概要 - td あなたのアプリの簡単な説明や紹介。 - tr - td コールバックURL - td ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。あなたのアプリがWebサービスである場合に有用です。 - tr - td 権限 - td あなたのアプリが要求する権限。ここで要求した機能だけがAPIからアクセスできます。 - - p 登録が済むとあなたのアプリのシークレットキーが入手できます。このシークレットキーは後で使用します。 - div.ui.info.warn: p %fa:exclamation-triangle%アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。 - - section - h3 2.ユーザーに認証させる - p あなたのアプリを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。 - p - | 認証セッションを開始するには、#{common.config.api_url}/auth/session/generate へパラメータに appSecret としてシークレットキーを含めたリクエストを送信します。 - | リクエスト形式はJSONで、メソッドはPOSTです。 - | レスポンスとして認証セッションのトークンや認証フォームのURLが取得できるので、認証フォームのURLをブラウザで表示し、ユーザーにフォームを提示してください。 - - p - | あなたのアプリがコールバックURLを設定している場合、 - | ユーザーがあなたのアプリの連携を許可すると設定しているコールバックURLに token という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。 - - p - | あなたのアプリがコールバックURLを設定していない場合、ユーザーがあなたのアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。 - - section - h3 3.ユーザーのアクセストークンを取得する - p ユーザーが連携を許可したら、#{common.config.api_url}/auth/session/userkey へ次のパラメータを含むリクエストを送信します: - table - thead - tr - th 名前 - th 型 - th 説明 - tbody - tr - td appSecret - td string - td あなたのアプリのシークレットキー - tr - td token - td string - td セッションのトークン - p 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます! - - p アクセストークンが取得できたら、「ユーザーのアクセストークン+あなたのアプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めると、APIにアクセスすることができます。 - - p 「i」パラメータの生成方法を擬似コードで表すと次のようになります: - pre: code - | const i = sha256(accessToken + secretKey); - - p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。 - -section - h2 Misskey APIの利用 - p APIはすべてリクエストのパラメータ・レスポンスともにJSON形式です。また、すべてのエンドポイントはPOSTメソッドのみ受け付けます。 - p APIリファレンスもご確認ください。 - - section - h3 レートリミット - p Misskey APIにはレートリミットがあり、短時間のうちに多数のリクエストを送信すると、一定時間APIを利用することができなくなることがあります。 diff --git a/src/client/docs/api/endpoints/notes/create.yaml b/src/client/docs/api/endpoints/notes/create.yaml deleted file mode 100644 index 04ada2ecd5..0000000000 --- a/src/client/docs/api/endpoints/notes/create.yaml +++ /dev/null @@ -1,59 +0,0 @@ -endpoint: "notes/create" - -desc: - ja: "投稿します。" - en: "Compose new note." - -params: - - name: "text" - type: "string" - optional: true - desc: - ja: "投稿の本文" - en: "The text of your note" - - name: "cw" - type: "string" - optional: true - desc: - ja: "コンテンツの警告。このパラメータを指定すると設定したテキストで投稿のコンテンツを隠す事が出来ます。" - en: "Content Warning" - - name: "mediaIds" - type: "id(DriveFile)[]" - optional: true - desc: - ja: "添付するメディア(1~4つ)" - en: "Media you want to attach (1~4)" - - name: "replyId" - type: "id(Note)" - optional: true - desc: - ja: "返信する投稿" - en: "The note you want to reply" - - name: "renoteId" - type: "id(Note)" - optional: true - desc: - ja: "引用する投稿" - en: "The note you want to quote" - - name: "poll" - type: "object" - optional: true - desc: - ja: "投票" - en: "The poll" - defName: "poll" - def: - - name: "choices" - type: "string[]" - optional: false - desc: - ja: "投票の選択肢" - en: "Choices of a poll" - -res: - - name: "createdNote" - type: "entity(Note)" - optional: false - desc: - ja: "作成した投稿" - en: "A note that created" diff --git a/src/client/docs/api/endpoints/notes/timeline.yaml b/src/client/docs/api/endpoints/notes/timeline.yaml deleted file mode 100644 index 71c346f355..0000000000 --- a/src/client/docs/api/endpoints/notes/timeline.yaml +++ /dev/null @@ -1,32 +0,0 @@ -endpoint: "notes/timeline" - -desc: - ja: "タイムラインを取得します。" - en: "Get your timeline." - -params: - - name: "limit" - type: "number" - optional: true - desc: - ja: "取得する最大の数" - - name: "sinceId" - type: "id(Note)" - optional: true - desc: - ja: "指定すると、この投稿を基点としてより新しい投稿を取得します" - - name: "untilId" - type: "id(Note)" - optional: true - desc: - ja: "指定すると、この投稿を基点としてより古い投稿を取得します" - - name: "sinceDate" - type: "number" - optional: true - desc: - ja: "指定した時間を基点としてより新しい投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。" - - name: "untilDate" - type: "number" - optional: true - desc: - ja: "指定した時間を基点としてより古い投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。" diff --git a/src/client/docs/api/endpoints/style.styl b/src/client/docs/api/endpoints/style.styl deleted file mode 100644 index 2af9fe9a77..0000000000 --- a/src/client/docs/api/endpoints/style.styl +++ /dev/null @@ -1,21 +0,0 @@ -@import "../style" - -#url - padding 8px 12px 8px 8px - font-family Consolas, 'Courier New', Courier, Monaco, monospace - color #fff - background #222e40 - border-radius 4px - - > .method - display inline-block - margin 0 8px 0 0 - padding 0 6px - color #f4fcff - background #17afc7 - border-radius 4px - user-select none - pointer-events none - - > .host - opacity 0.7 diff --git a/src/client/docs/api/endpoints/view.pug b/src/client/docs/api/endpoints/view.pug deleted file mode 100644 index f8795c8442..0000000000 --- a/src/client/docs/api/endpoints/view.pug +++ /dev/null @@ -1,32 +0,0 @@ -extends ../../layout.pug -include ../mixins - -block meta - link(rel="stylesheet" href="/docs/assets/api/endpoints/style.css") - -block main - h1= endpoint - - p#url - span.method POST - span.host - = url.host - | / - span.path= url.path - - p#desc= desc[lang] || desc['ja'] - - section - h2 %i18n:docs.api.endpoints.params% - +propTable(params) - - if paramDefs - each paramDef in paramDefs - section(id= paramDef.name) - h3= paramDef.name - +propTable(paramDef.params) - - if res - section - h2 %i18n:docs.api.endpoints.res% - +propTable(res) diff --git a/src/client/docs/api/entities/drive-file.yaml b/src/client/docs/api/entities/drive-file.yaml deleted file mode 100644 index 02ab0d608e..0000000000 --- a/src/client/docs/api/entities/drive-file.yaml +++ /dev/null @@ -1,73 +0,0 @@ -name: "DriveFile" - -desc: - ja: "ドライブのファイル。" - en: "A file of Drive." - -props: - - name: "id" - type: "id" - optional: false - desc: - ja: "ファイルID" - en: "The ID of this file" - - name: "createdAt" - type: "date" - optional: false - desc: - ja: "アップロード日時" - en: "The upload date of this file" - - name: "userId" - type: "id(User)" - optional: false - desc: - ja: "所有者ID" - en: "The ID of the owner of this file" - - name: "user" - type: "entity(User)" - optional: true - desc: - ja: "所有者" - en: "The owner of this file" - - name: "name" - type: "string" - optional: false - desc: - ja: "ファイル名" - en: "The name of this file" - - name: "md5" - type: "string" - optional: false - desc: - ja: "ファイルのMD5ハッシュ値" - en: "The md5 hash value of this file" - - name: "type" - type: "string" - optional: false - desc: - ja: "ファイルの種類" - en: "The type of this file" - - name: "datasize" - type: "number" - optional: false - desc: - ja: "ファイルサイズ(bytes)" - en: "The size of this file (bytes)" - - name: "url" - type: "string" - optional: false - desc: - ja: "ファイルのURL" - en: "The URL of this file" - - name: "folderId" - type: "id(DriveFolder)" - optional: true - desc: - ja: "フォルダID" - en: "The ID of the folder of this file" - - name: "folder" - type: "entity(DriveFolder)" - optional: true - desc: - ja: "フォルダ" - en: "The folder of this file" diff --git a/src/client/docs/api/entities/note.yaml b/src/client/docs/api/entities/note.yaml deleted file mode 100644 index 6fd26543bb..0000000000 --- a/src/client/docs/api/entities/note.yaml +++ /dev/null @@ -1,168 +0,0 @@ -name: "Note" - -desc: - ja: "投稿。" - en: "A note." - -props: - - name: "id" - type: "id" - optional: false - desc: - ja: "投稿ID" - en: "The ID of this note" - - name: "createdAt" - type: "date" - optional: false - desc: - ja: "投稿日時" - en: "The posted date of this note" - - name: "viaMobile" - type: "boolean" - optional: true - desc: - ja: "モバイル端末から投稿したか否か(自己申告であることに留意)" - en: "Whether this note sent via a mobile device" - - name: "text" - type: "string" - optional: true - desc: - ja: "投稿の本文 (ローカルの場合Markdown風のフォーマット)" - en: "The text of this note (in Markdown like format if local)" - - name: "mediaIds" - type: "id(DriveFile)[]" - optional: true - desc: - ja: "添付されているメディアのID (なければレスポンスでは空配列)" - en: "The IDs of the attached media (empty array for response if no media is attached)" - - name: "media" - type: "entity(DriveFile)[]" - optional: true - desc: - ja: "添付されているメディア" - en: "The attached media" - - name: "userId" - type: "id(User)" - optional: false - desc: - ja: "投稿者ID" - en: "The ID of author of this note" - - name: "user" - type: "entity(User)" - optional: true - desc: - ja: "投稿者" - en: "The author of this note" - - name: "myReaction" - type: "string" - optional: true - desc: - ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>" - en: "The your <a href='/docs/api/reactions'>reaction</a> of this note" - - name: "reactionCounts" - type: "object" - optional: false - desc: - ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト" - - name: "replyId" - type: "id(Note)" - optional: true - desc: - ja: "返信した投稿のID" - en: "The ID of the replyed note" - - name: "reply" - type: "entity(Note)" - optional: true - desc: - ja: "返信した投稿" - en: "The replyed note" - - name: "renoteId" - type: "id(Note)" - optional: true - desc: - ja: "引用した投稿のID" - en: "The ID of the quoted note" - - name: "renote" - type: "entity(Note)" - optional: true - desc: - ja: "引用した投稿" - en: "The quoted note" - - name: "poll" - type: "object" - optional: true - desc: - ja: "投票" - en: "The poll" - defName: "poll" - def: - - name: "choices" - type: "object[]" - optional: false - desc: - ja: "投票の選択肢" - en: "The choices of this poll" - defName: "choice" - def: - - name: "id" - type: "number" - optional: false - desc: - ja: "選択肢ID" - en: "The ID of this choice" - - name: "isVoted" - type: "boolean" - optional: true - desc: - ja: "自分がこの選択肢に投票したかどうか" - en: "Whether you voted to this choice" - - name: "text" - type: "string" - optional: false - desc: - ja: "選択肢本文" - en: "The text of this choice" - - name: "votes" - type: "number" - optional: false - desc: - ja: "この選択肢に投票された数" - en: "The number voted for this choice" - - name: "geo" - type: "object" - optional: true - desc: - ja: "位置情報" - en: "Geo location" - defName: "geo" - def: - - name: "coordinates" - type: "number[]" - optional: false - desc: - ja: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。" - - name: "altitude" - type: "number" - optional: false - desc: - ja: "高度。メートル単位で表す。" - - name: "accuracy" - type: "number" - optional: false - desc: - ja: "緯度、経度の精度。メートル単位で表す。" - - name: "altitudeAccuracy" - type: "number" - optional: false - desc: - ja: "高度の精度。メートル単位で表す。" - - name: "heading" - type: "number" - optional: false - desc: - ja: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。" - - name: "speed" - type: "number" - optional: false - desc: - ja: "速度。メートル / 秒数で表す。" diff --git a/src/client/docs/api/entities/post.yaml b/src/client/docs/api/entities/post.yaml deleted file mode 100644 index 6fd26543bb..0000000000 --- a/src/client/docs/api/entities/post.yaml +++ /dev/null @@ -1,168 +0,0 @@ -name: "Note" - -desc: - ja: "投稿。" - en: "A note." - -props: - - name: "id" - type: "id" - optional: false - desc: - ja: "投稿ID" - en: "The ID of this note" - - name: "createdAt" - type: "date" - optional: false - desc: - ja: "投稿日時" - en: "The posted date of this note" - - name: "viaMobile" - type: "boolean" - optional: true - desc: - ja: "モバイル端末から投稿したか否か(自己申告であることに留意)" - en: "Whether this note sent via a mobile device" - - name: "text" - type: "string" - optional: true - desc: - ja: "投稿の本文 (ローカルの場合Markdown風のフォーマット)" - en: "The text of this note (in Markdown like format if local)" - - name: "mediaIds" - type: "id(DriveFile)[]" - optional: true - desc: - ja: "添付されているメディアのID (なければレスポンスでは空配列)" - en: "The IDs of the attached media (empty array for response if no media is attached)" - - name: "media" - type: "entity(DriveFile)[]" - optional: true - desc: - ja: "添付されているメディア" - en: "The attached media" - - name: "userId" - type: "id(User)" - optional: false - desc: - ja: "投稿者ID" - en: "The ID of author of this note" - - name: "user" - type: "entity(User)" - optional: true - desc: - ja: "投稿者" - en: "The author of this note" - - name: "myReaction" - type: "string" - optional: true - desc: - ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>" - en: "The your <a href='/docs/api/reactions'>reaction</a> of this note" - - name: "reactionCounts" - type: "object" - optional: false - desc: - ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト" - - name: "replyId" - type: "id(Note)" - optional: true - desc: - ja: "返信した投稿のID" - en: "The ID of the replyed note" - - name: "reply" - type: "entity(Note)" - optional: true - desc: - ja: "返信した投稿" - en: "The replyed note" - - name: "renoteId" - type: "id(Note)" - optional: true - desc: - ja: "引用した投稿のID" - en: "The ID of the quoted note" - - name: "renote" - type: "entity(Note)" - optional: true - desc: - ja: "引用した投稿" - en: "The quoted note" - - name: "poll" - type: "object" - optional: true - desc: - ja: "投票" - en: "The poll" - defName: "poll" - def: - - name: "choices" - type: "object[]" - optional: false - desc: - ja: "投票の選択肢" - en: "The choices of this poll" - defName: "choice" - def: - - name: "id" - type: "number" - optional: false - desc: - ja: "選択肢ID" - en: "The ID of this choice" - - name: "isVoted" - type: "boolean" - optional: true - desc: - ja: "自分がこの選択肢に投票したかどうか" - en: "Whether you voted to this choice" - - name: "text" - type: "string" - optional: false - desc: - ja: "選択肢本文" - en: "The text of this choice" - - name: "votes" - type: "number" - optional: false - desc: - ja: "この選択肢に投票された数" - en: "The number voted for this choice" - - name: "geo" - type: "object" - optional: true - desc: - ja: "位置情報" - en: "Geo location" - defName: "geo" - def: - - name: "coordinates" - type: "number[]" - optional: false - desc: - ja: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。" - - name: "altitude" - type: "number" - optional: false - desc: - ja: "高度。メートル単位で表す。" - - name: "accuracy" - type: "number" - optional: false - desc: - ja: "緯度、経度の精度。メートル単位で表す。" - - name: "altitudeAccuracy" - type: "number" - optional: false - desc: - ja: "高度の精度。メートル単位で表す。" - - name: "heading" - type: "number" - optional: false - desc: - ja: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。" - - name: "speed" - type: "number" - optional: false - desc: - ja: "速度。メートル / 秒数で表す。" diff --git a/src/client/docs/api/entities/style.styl b/src/client/docs/api/entities/style.styl deleted file mode 100644 index bddf0f53ab..0000000000 --- a/src/client/docs/api/entities/style.styl +++ /dev/null @@ -1 +0,0 @@ -@import "../style" diff --git a/src/client/docs/api/entities/user.yaml b/src/client/docs/api/entities/user.yaml deleted file mode 100644 index cccf42f221..0000000000 --- a/src/client/docs/api/entities/user.yaml +++ /dev/null @@ -1,173 +0,0 @@ -name: "User" - -desc: - ja: "ユーザー。" - en: "A user." - -props: - - name: "id" - type: "id" - optional: false - desc: - ja: "ユーザーID" - en: "The ID of this user" - - name: "createdAt" - type: "date" - optional: false - desc: - ja: "アカウント作成日時" - en: "The registered date of this user" - - name: "username" - type: "string" - optional: false - desc: - ja: "ユーザー名" - en: "The username of this user" - - name: "description" - type: "string" - optional: false - desc: - ja: "アカウントの説明(自己紹介)" - en: "The description of this user" - - name: "avatarId" - type: "id(DriveFile)" - optional: true - desc: - ja: "アバターのID" - en: "The ID of the avatar of this user" - - name: "avatarUrl" - type: "string" - optional: false - desc: - ja: "アバターのURL" - en: "The URL of the avatar of this user" - - name: "bannerId" - type: "id(DriveFile)" - optional: true - desc: - ja: "バナーのID" - en: "The ID of the banner of this user" - - name: "bannerUrl" - type: "string" - optional: false - desc: - ja: "バナーのURL" - en: "The URL of the banner of this user" - - name: "followersCount" - type: "number" - optional: false - desc: - ja: "フォロワーの数" - en: "The number of the followers for this user" - - name: "followingCount" - type: "number" - optional: false - desc: - ja: "フォローしているユーザーの数" - en: "The number of the following users for this user" - - name: "isFollowing" - type: "boolean" - optional: true - desc: - ja: "自分がこのユーザーをフォローしているか" - - name: "isFollowed" - type: "boolean" - optional: true - desc: - ja: "自分がこのユーザーにフォローされているか" - - name: "isMuted" - type: "boolean" - optional: true - desc: - ja: "自分がこのユーザーをミュートしているか" - en: "Whether you muted this user" - - name: "notesCount" - type: "number" - optional: false - desc: - ja: "投稿の数" - en: "The number of the notes of this user" - - name: "pinnedNote" - type: "entity(Note)" - optional: true - desc: - ja: "ピン留めされた投稿" - en: "The pinned note of this user" - - name: "pinnedNoteId" - type: "id(Note)" - optional: true - desc: - ja: "ピン留めされた投稿のID" - en: "The ID of the pinned note of this user" - - name: "driveCapacity" - type: "number" - optional: false - desc: - ja: "ドライブの容量(bytes)" - en: "The capacity of drive of this user (bytes)" - - name: "host" - type: "string | null" - optional: false - desc: - ja: "ホスト (例: example.com:3000)" - en: "Host (e.g. example.com:3000)" - - name: "account" - type: "object" - optional: false - desc: - ja: "このサーバーにおけるアカウント" - en: "The account of this user on this server" - defName: "account" - def: - - name: "lastUsedAt" - type: "date" - optional: false - desc: - ja: "最終利用日時" - en: "The last used date of this user" - - name: "isBot" - type: "boolean" - optional: true - desc: - ja: "botか否か(自己申告であることに留意)" - en: "Whether is bot or not" - - name: "twitter" - type: "object" - optional: true - desc: - ja: "連携されているTwitterアカウント情報" - en: "The info of the connected twitter account of this user" - defName: "twitter" - def: - - name: "userId" - type: "string" - optional: false - desc: - ja: "ユーザーID" - en: "The user ID" - - name: "screenName" - type: "string" - optional: false - desc: - ja: "ユーザー名" - en: "The screen name of this user" - - name: "profile" - type: "object" - optional: false - desc: - ja: "プロフィール" - en: "The profile of this user" - defName: "profile" - def: - - name: "location" - type: "string" - optional: true - desc: - ja: "場所" - en: "The location of this user" - - name: "birthday" - type: "string" - optional: true - desc: - ja: "誕生日 (YYYY-MM-DD)" - en: "The birthday of this user (YYYY-MM-DD)" diff --git a/src/client/docs/api/entities/view.pug b/src/client/docs/api/entities/view.pug deleted file mode 100644 index ac938151a7..0000000000 --- a/src/client/docs/api/entities/view.pug +++ /dev/null @@ -1,20 +0,0 @@ -extends ../../layout.pug -include ../mixins - -block meta - link(rel="stylesheet" href="/docs/assets/api/entities/style.css") - -block main - h1= name - - p#desc= desc[lang] || desc['ja'] - - section - h2 %i18n:docs.api.entities.properties% - +propTable(props) - - if propDefs - each propDef in propDefs - section(id= propDef.name) - h3= propDef.name - +propTable(propDef.params) diff --git a/src/client/docs/api/gulpfile.ts b/src/client/docs/api/gulpfile.ts deleted file mode 100644 index 0eb8b88287..0000000000 --- a/src/client/docs/api/gulpfile.ts +++ /dev/null @@ -1,193 +0,0 @@ -/** - * Gulp tasks - */ - -import * as fs from 'fs'; -import * as path from 'path'; -import * as glob from 'glob'; -import * as gulp from 'gulp'; -import * as pug from 'pug'; -import * as yaml from 'js-yaml'; -import * as mkdirp from 'mkdirp'; - -import locales from '../../../../locales'; -import I18nReplacer from '../../../build/i18n'; -import fa from '../../../build/fa'; -import config from './../../../config'; - -import generateVars from '../vars'; - -const langs = Object.keys(locales); - -const kebab = (string: string) => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase(); - -// WIP type -const parseParam = (param: any) => { - const id = param.type.match(/^id\((.+?)\)|^id/); - const entity = param.type.match(/^entity\((.+?)\)/); - const isObject = /^object/.test(param.type); - const isDate = /^date/.test(param.type); - const isArray = /\[\]$/.test(param.type); - if (id) { - param.kind = 'id'; - param.type = 'string'; - param.entity = id[1]; - if (isArray) { - param.type += '[]'; - } - } - if (entity) { - param.kind = 'entity'; - param.type = 'object'; - param.entity = entity[1]; - if (isArray) { - param.type += '[]'; - } - } - if (isObject) { - param.kind = 'object'; - } - if (isDate) { - param.kind = 'date'; - param.type = 'string'; - if (isArray) { - param.type += '[]'; - } - } - - return param; -}; - -const sortParams = (params: Array<{name: string}>) => { - params.sort((a, b) => { - if (a.name < b.name) - return -1; - if (a.name > b.name) - return 1; - return 0; - }); - return params; -}; - -// WIP type -const extractDefs = (params: any[]) => { - let defs: any[] = []; - - params.forEach(param => { - if (param.def) { - defs.push({ - name: param.defName, - params: sortParams(param.def.map((p: any) => parseParam(p))) - }); - - const childDefs = extractDefs(param.def); - - defs = defs.concat(childDefs); - } - }); - - return sortParams(defs); -}; - -gulp.task('doc:api', [ - 'doc:api:endpoints', - 'doc:api:entities' -]); - -gulp.task('doc:api:endpoints', async () => { - const commonVars = await generateVars(); - glob('./src/client/docs/api/endpoints/**/*.yaml', (globErr, files) => { - if (globErr) { - console.error(globErr); - return; - } - //console.log(files); - files.forEach(file => { - const ep: any = yaml.safeLoad(fs.readFileSync(file, 'utf-8')); - const vars = { - endpoint: ep.endpoint, - url: { - host: config.api_url, - path: ep.endpoint - }, - desc: ep.desc, - // @ts-ignore - params: sortParams(ep.params.map(p => parseParam(p))), - paramDefs: extractDefs(ep.params), - // @ts-ignore - res: ep.res ? sortParams(ep.res.map(p => parseParam(p))) : null, - resDefs: ep.res ? extractDefs(ep.res) : null, - }; - langs.forEach(lang => { - pug.renderFile('./src/client/docs/api/endpoints/view.pug', Object.assign({}, vars, { - lang, - title: ep.endpoint, - src: `https://github.com/syuilo/misskey/tree/master/src/client/docs/api/endpoints/${ep.endpoint}.yaml`, - kebab, - common: commonVars - }), (renderErr, html) => { - if (renderErr) { - console.error(renderErr); - return; - } - const i18n = new I18nReplacer(lang); - html = html.replace(i18n.pattern, i18n.replacement); - html = fa(html); - const htmlPath = `./built/client/docs/${lang}/api/endpoints/${ep.endpoint}.html`; - mkdirp(path.dirname(htmlPath), (mkdirErr) => { - if (mkdirErr) { - console.error(mkdirErr); - return; - } - fs.writeFileSync(htmlPath, html, 'utf-8'); - }); - }); - }); - }); - }); -}); - -gulp.task('doc:api:entities', async () => { - const commonVars = await generateVars(); - glob('./src/client/docs/api/entities/**/*.yaml', (globErr, files) => { - if (globErr) { - console.error(globErr); - return; - } - files.forEach(file => { - const entity = yaml.safeLoad(fs.readFileSync(file, 'utf-8')) as any; - const vars = { - name: entity.name, - desc: entity.desc, - // WIP type - props: sortParams(entity.props.map((p: any) => parseParam(p))), - propDefs: extractDefs(entity.props), - }; - langs.forEach(lang => { - pug.renderFile('./src/client/docs/api/entities/view.pug', Object.assign({}, vars, { - lang, - title: entity.name, - src: `https://github.com/syuilo/misskey/tree/master/src/client/docs/api/entities/${kebab(entity.name)}.yaml`, - kebab, - common: commonVars - }), (renderErr, html) => { - if (renderErr) { - console.error(renderErr); - return; - } - const i18n = new I18nReplacer(lang); - html = html.replace(i18n.pattern, i18n.replacement); - html = fa(html); - const htmlPath = `./built/client/docs/${lang}/api/entities/${kebab(entity.name)}.html`; - mkdirp(path.dirname(htmlPath), (mkdirErr) => { - if (mkdirErr) { - console.error(mkdirErr); - return; - } - fs.writeFileSync(htmlPath, html, 'utf-8'); - }); - }); - }); - }); - }); -}); diff --git a/src/client/docs/api/mixins.pug b/src/client/docs/api/mixins.pug deleted file mode 100644 index 913135a85f..0000000000 --- a/src/client/docs/api/mixins.pug +++ /dev/null @@ -1,37 +0,0 @@ -mixin propTable(props) - table.props - thead: tr - th %i18n:docs.api.props.name% - th %i18n:docs.api.props.type% - th %i18n:docs.api.props.optional% - th %i18n:docs.api.props.description% - tbody - each prop in props - tr - td.name= prop.name - td.type - i= prop.type - if prop.kind == 'id' - if prop.entity - | ( - a(href=`/docs/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity - | ID) - else - | (ID) - else if prop.kind == 'entity' - | ( - a(href=`/docs/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity - | ) - else if prop.kind == 'object' - if prop.def - | ( - a(href=`#${prop.defName}`)= prop.defName - | ) - else if prop.kind == 'date' - | (Date) - td.optional - if prop.optional - | %i18n:docs.api.props.yes% - else - | %i18n:docs.api.props.no% - td.desc!= prop.desc[lang] || prop.desc['ja'] diff --git a/src/client/docs/api/style.styl b/src/client/docs/api/style.styl deleted file mode 100644 index 3675a4da6f..0000000000 --- a/src/client/docs/api/style.styl +++ /dev/null @@ -1,11 +0,0 @@ -@import "../style" - -table.props - .name - font-weight bold - - .name - .type - .optional - font-family Consolas, 'Courier New', Courier, Monaco, monospace - diff --git a/src/client/docs/follow.ja.pug b/src/client/docs/follow.ja.pug deleted file mode 100644 index f0e83bc8fd..0000000000 --- a/src/client/docs/follow.ja.pug +++ /dev/null @@ -1,9 +0,0 @@ -h1 フォロー -p ユーザーをフォローすると、タイムラインにそのユーザーの投稿が表示されるようになります。ただし、他のユーザーに対する返信は含まれません。 -p ユーザーをフォローするには、ユーザーページの「フォロー」ボタンをクリックします。フォローを解除するには、もう一度クリックします。 - -section - h2 ストーキング - p ユーザーをフォローしている状態では、さらに「ストーキング」モードをオンにすることができます。ストーキングを行うと、タイムラインにそのユーザーの全ての投稿が表示されるようになります。つまり、他のユーザーに対する返信も含まれることになります。 - p ストーキングするには、ユーザーページの「ストークする」をクリックします。ストーキングをやめるには、もう一度クリックします。 - p ストーキングしていることは相手に通知されません。 diff --git a/src/client/docs/gulpfile.ts b/src/client/docs/gulpfile.ts deleted file mode 100644 index 4683a04659..0000000000 --- a/src/client/docs/gulpfile.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Gulp tasks - */ - -import * as fs from 'fs'; -import * as path from 'path'; -import * as glob from 'glob'; -import * as gulp from 'gulp'; -import * as pug from 'pug'; -import * as mkdirp from 'mkdirp'; -const stylus = require('gulp-stylus'); -const cssnano = require('gulp-cssnano'); - -import I18nReplacer from '../../build/i18n'; -import fa from '../../build/fa'; -import generateVars from './vars'; - -require('./api/gulpfile.ts'); - -gulp.task('doc', [ - 'doc:docs', - 'doc:api', - 'doc:styles' -]); - -gulp.task('doc:docs', async () => { - const commonVars = await generateVars(); - - glob('./src/client/docs/**/*.*.pug', (globErr, files) => { - if (globErr) { - console.error(globErr); - return; - } - files.forEach(file => { - const [, name, lang] = file.match(/docs\/(.+?)\.(.+?)\.pug$/); - const vars = { - common: commonVars, - lang: lang, - title: fs.readFileSync(file, 'utf-8').match(/^h1 (.+?)\r?\n/)[1], - src: `https://github.com/syuilo/misskey/tree/master/src/client/docs/${name}.${lang}.pug`, - }; - pug.renderFile(file, vars, (renderErr, content) => { - if (renderErr) { - console.error(renderErr); - return; - } - - pug.renderFile('./src/client/docs/layout.pug', Object.assign({}, vars, { - content - }), (renderErr2, html) => { - if (renderErr2) { - console.error(renderErr2); - return; - } - const i18n = new I18nReplacer(lang); - html = html.replace(i18n.pattern, i18n.replacement); - html = fa(html); - const htmlPath = `./built/client/docs/${lang}/${name}.html`; - mkdirp(path.dirname(htmlPath), (mkdirErr) => { - if (mkdirErr) { - console.error(mkdirErr); - return; - } - fs.writeFileSync(htmlPath, html, 'utf-8'); - }); - }); - }); - }); - }); -}); - -gulp.task('doc:styles', () => - gulp.src('./src/client/docs/**/*.styl') - .pipe(stylus()) - .pipe((cssnano as any)()) - .pipe(gulp.dest('./built/client/docs/assets/')) -); diff --git a/src/client/docs/index.en.pug b/src/client/docs/index.en.pug deleted file mode 100644 index 1fcc870d3d..0000000000 --- a/src/client/docs/index.en.pug +++ /dev/null @@ -1,3 +0,0 @@ -h1 Misskey Docs - -p Welcome to docs of Misskey. diff --git a/src/client/docs/index.ja.pug b/src/client/docs/index.ja.pug deleted file mode 100644 index 4a0bf7fa1d..0000000000 --- a/src/client/docs/index.ja.pug +++ /dev/null @@ -1,3 +0,0 @@ -h1 Misskey ドキュメント - -p Misskeyのドキュメントへようこそ diff --git a/src/client/docs/layout.pug b/src/client/docs/layout.pug deleted file mode 100644 index 1d9ebcb4cd..0000000000 --- a/src/client/docs/layout.pug +++ /dev/null @@ -1,41 +0,0 @@ -doctype html - -html(lang= lang) - head - meta(charset="UTF-8") - meta(name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no") - title - | #{title} | Misskey Docs - link(rel="stylesheet" href="/docs/assets/style.css") - block meta - - //- FontAwesome style - style #{common.facss} - - body - nav - ul - each doc in common.docs - li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja'] - section - h2 API - ul - li Entities - ul - each entity in common.entities - li: a(href=`/docs/${lang}/api/entities/${common.kebab(entity)}`)= entity - li Endpoints - ul - each endpoint in common.endpoints - li: a(href=`/docs/${lang}/api/endpoints/${common.kebab(endpoint)}`)= endpoint - main - article - block main - if content - | !{content} - - footer - p - | %i18n:docs.edit-this-page-on-github% - a(href=src target="_blank") %i18n:docs.edit-this-page-on-github-link% - small= common.copyright diff --git a/src/client/docs/license.en.pug b/src/client/docs/license.en.pug deleted file mode 100644 index 45d8b76473..0000000000 --- a/src/client/docs/license.en.pug +++ /dev/null @@ -1,17 +0,0 @@ -h1 License - -div!= common.license - -details - summary Libraries - - section - h2 Libraries - - each dependency, name in common.dependencies - details - summary= name - - section - h3= name - pre= dependency.licenseText diff --git a/src/client/docs/license.ja.pug b/src/client/docs/license.ja.pug deleted file mode 100644 index 6eb9ac308e..0000000000 --- a/src/client/docs/license.ja.pug +++ /dev/null @@ -1,17 +0,0 @@ -h1 ライセンス - -div!= common.license - -details - summary サードパーティ - - section - h2 サードパーティ - - each dependency, name in common.dependencies - details - summary= name - - section - h3= name - pre= dependency.licenseText diff --git a/src/client/docs/mute.ja.pug b/src/client/docs/mute.ja.pug deleted file mode 100644 index 807f7b67a7..0000000000 --- a/src/client/docs/mute.ja.pug +++ /dev/null @@ -1,13 +0,0 @@ -h1 ミュート - -p ユーザーページから、そのユーザーをミュートすることができます。 - -p ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります: -ul - li タイムラインや投稿の検索結果内の、そのユーザーの投稿(およびそれらの投稿に対する返信やRenote) - li そのユーザーからの通知 - li メッセージ履歴一覧内の、そのユーザーとのメッセージ履歴 - -p ミュートを行ったことは相手に通知されず、ミュートされていることを知ることもできません。 - -p 設定>ミュート から、自分がミュートしているユーザー一覧を確認することができます。 diff --git a/src/client/docs/search.ja.pug b/src/client/docs/search.ja.pug deleted file mode 100644 index fc62d16cae..0000000000 --- a/src/client/docs/search.ja.pug +++ /dev/null @@ -1,120 +0,0 @@ -h1 検索 - -p 投稿を検索することができます。 -p - | キーワードを半角スペースで区切ると、and検索になります。 - | 例えば、「git コミット」と検索すると、「gitで編集したファイルの特定の行だけコミットする方法がわからない」などがマッチします。 - -section - h2 キーワードの除外 - p キーワードの前に「-」(ハイフン)をプリフィクスすると、そのキーワードを含まない投稿に限定します。 - p 例えば、「gitというキーワードを含むが、コミットというキーワードは含まない投稿」を検索したい場合、クエリは以下のようになります: - code git -コミット - -section - h2 完全一致 - p テキストを「"""」で囲むと、そのテキストと完全に一致する投稿を検索します。 - p 例えば、「"""にゃーん"""」と検索すると、「にゃーん」という投稿のみがヒットし、「にゃーん…」という投稿はヒットしません。 - -section - h2 タグ - p キーワードの前に「#」(シャープ)をプリフィクスすると、そのキーワードと一致するタグを持つ投稿に限定します。 - -section - h2 オプション - p - | オプションを使用して、より高度な検索を行えます。 - | オプションを指定するには、「オプション名:値」という形式でクエリに含めます。 - p 利用可能なオプション一覧です: - - table - thead - tr - th 名前 - th 説明 - tbody - tr - td user - td - | 指定されたユーザー名のユーザーの投稿に限定します。 - | 「,」(カンマ)で区切って、複数ユーザーを指定することもできます。 - br - | 例えば、 - code user:himawari,sakurako - | と検索すると「@himawariまたは@sakurakoの投稿」だけに限定します。 - | (つまりユーザーのホワイトリストです) - tr - td exclude_user - td - | 指定されたユーザー名のユーザーの投稿を除外します。 - | 「,」(カンマ)で区切って、複数ユーザーを指定することもできます。 - br - | 例えば、 - code exclude_user:akari,chinatsu - | と検索すると「@akariまたは@chinatsu以外の投稿」に限定します。 - | (つまりユーザーのブラックリストです) - tr - td follow - td - | true ... フォローしているユーザーに限定。 - br - | false ... フォローしていないユーザーに限定。 - br - | null ... 特に限定しない(デフォルト) - tr - td mute - td - | mute_all ... ミュートしているユーザーの投稿とその投稿に対する返信やRenoteを除外する(デフォルト) - br - | mute_related ... ミュートしているユーザーの投稿に対する返信やRenoteだけ除外する - br - | mute_direct ... ミュートしているユーザーの投稿だけ除外する - br - | disabled ... ミュートしているユーザーの投稿とその投稿に対する返信やRenoteも含める - br - | direct_only ... ミュートしているユーザーの投稿だけに限定 - br - | related_only ... ミュートしているユーザーの投稿に対する返信やRenoteだけに限定 - br - | all_only ... ミュートしているユーザーの投稿とその投稿に対する返信やRenoteに限定 - tr - td reply - td - | true ... 返信に限定。 - br - | false ... 返信でない投稿に限定。 - br - | null ... 特に限定しない(デフォルト) - tr - td renote - td - | true ... Renoteに限定。 - br - | false ... Renoteでない投稿に限定。 - br - | null ... 特に限定しない(デフォルト) - tr - td media - td - | true ... メディアが添付されている投稿に限定。 - br - | false ... メディアが添付されていない投稿に限定。 - br - | null ... 特に限定しない(デフォルト) - tr - td poll - td - | true ... 投票が添付されている投稿に限定。 - br - | false ... 投票が添付されていない投稿に限定。 - br - | null ... 特に限定しない(デフォルト) - tr - td until - td 上限の日時。(YYYY-MM-DD) - tr - td since - td 下限の日時。(YYYY-MM-DD) - - p 例えば、「@syuiloの2017年11月1日から2017年12月31日までの『Misskey』というテキストを含む返信ではない投稿」を検索したい場合、クエリは以下のようになります: - code user:syuilo since:2017-11-01 until:2017-12-31 reply:false Misskey diff --git a/src/client/docs/style.styl b/src/client/docs/style.styl deleted file mode 100644 index bc165f8728..0000000000 --- a/src/client/docs/style.styl +++ /dev/null @@ -1,120 +0,0 @@ -@import "../style" -@import "./ui" - -body - margin 0 - color #34495e - word-break break-word - -main - margin 0 0 0 256px - padding 64px - width 100% - max-width 768px - - section - margin 32px 0 - - h1 - margin 0 0 24px 0 - padding 16px 0 - font-size 1.5em - border-bottom solid 2px #eee - - h2 - margin 0 0 24px 0 - padding 0 0 16px 0 - font-size 1.4em - border-bottom solid 1px #eee - - h3 - margin 0 - padding 0 - font-size 1.25em - - h4 - margin 0 - - p - margin 1em 0 - line-height 1.6em - - footer - margin 32px 0 0 0 - border-top solid 2px #eee - - > small - margin 16px 0 0 0 - color #aaa - -nav - display block - position fixed - z-index 10000 - top 0 - left 0 - width 256px - height 100% - overflow auto - padding 32px - background #fff - border-right solid 2px #eee - -@media (max-width 1025px) - main - margin 0 - max-width 100% - - nav - position relative - width 100% - max-height 128px - background #f9f9f9 - border-right none - -@media (max-width 768px) - main - padding 32px - -@media (max-width 512px) - main - padding 16px - -table - display block - width 100% - max-width 100% - overflow auto - border-spacing 0 - border-collapse collapse - - thead - font-weight bold - border-bottom solid 2px #eee - - tr - th - text-align left - - tbody - tr - &:nth-child(odd) - background #fbfbfb - - th, td - padding 8px 16px - min-width 128px - -code - display inline-block - padding 8px 10px - font-family Consolas, 'Courier New', Courier, Monaco, monospace - color #295c92 - background #f2f2f2 - border-radius 4px - -pre - overflow auto - - > code - display block diff --git a/src/client/docs/tou.ja.pug b/src/client/docs/tou.ja.pug deleted file mode 100644 index 7663258f82..0000000000 --- a/src/client/docs/tou.ja.pug +++ /dev/null @@ -1,3 +0,0 @@ -h1 利用規約 - -p 公序良俗に反する行為はおやめください。 diff --git a/src/client/docs/ui.styl b/src/client/docs/ui.styl deleted file mode 100644 index 8d5515712f..0000000000 --- a/src/client/docs/ui.styl +++ /dev/null @@ -1,19 +0,0 @@ -.ui.info - display block - margin 1em 0 - padding 0 1em - font-size 90% - color rgba(#000, 0.87) - background #f8f8f9 - border-radius 4px - overflow hidden - - > p - opacity 0.8 - - > [data-fa]:first-child - margin-right 0.25em - - &.warn - color #573a08 - background #FFFAF3 diff --git a/src/client/docs/vars.ts b/src/client/docs/vars.ts deleted file mode 100644 index 93082767e3..0000000000 --- a/src/client/docs/vars.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as fs from 'fs'; -import * as util from 'util'; -import * as glob from 'glob'; -import * as yaml from 'js-yaml'; -import * as licenseChecker from 'license-checker'; -import * as tmp from 'tmp'; - -import { fa } from '../../build/fa'; -import config from '../../config'; -import { licenseHtml } from '../../build/license'; -const constants = require('../../const.json'); - -export default async function(): Promise<{ [key: string]: any }> { - const vars = {} as { [key: string]: any }; - - const endpoints = glob.sync('./src/client/docs/api/endpoints/**/*.yaml'); - vars['endpoints'] = endpoints.map(ep => { - const _ep = yaml.safeLoad(fs.readFileSync(ep, 'utf-8')) as any; - return _ep.endpoint; - }); - - const entities = glob.sync('./src/client/docs/api/entities/**/*.yaml'); - vars['entities'] = entities.map(x => { - const _x = yaml.safeLoad(fs.readFileSync(x, 'utf-8')) as any; - return _x.name; - }); - - const docs = glob.sync('./src/client/docs/**/*.*.pug'); - vars['docs'] = {}; - docs.forEach(x => { - const [, name, lang] = x.match(/docs\/(.+?)\.(.+?)\.pug$/); - if (vars['docs'][name] == null) { - vars['docs'][name] = { - name, - title: {} - }; - } - vars['docs'][name]['title'][lang] = fs.readFileSync(x, 'utf-8').match(/^h1 (.+?)\r?\n/)[1]; - }); - - vars['kebab'] = (string: string) => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase(); - - vars['config'] = config; - - vars['copyright'] = constants.copyright; - - vars['facss'] = fa.dom.css(); - - vars['license'] = licenseHtml; - - const tmpObj = tmp.fileSync(); - fs.writeFileSync(tmpObj.name, JSON.stringify({ - licenseText: '' - }), 'utf-8'); - const dependencies = await util.promisify(licenseChecker.init).bind(licenseChecker)({ - start: __dirname + '/../../../', - customPath: tmpObj.name - }); - tmpObj.removeCallback(); - - vars['dependencies'] = dependencies; - - return vars; -} |