mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-24 03:43:09 +02:00
Merge branch 'develop'
This commit is contained in:
commit
7b44727b23
39 changed files with 556 additions and 260 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -5,6 +5,22 @@ If you encounter any problems with updating, please try the following:
|
|||
1. `npm run clean` or `npm run cleanall`
|
||||
2. Retry update (Don't forget `npm i`)
|
||||
|
||||
11.2.0 (2019/04/18)
|
||||
-------------------
|
||||
### Improvements
|
||||
* 検索で日付(日時)を入力するとタイムラインをその時点まで遡るように
|
||||
* APIコンソールでエンドポイントをサジェストするように
|
||||
* モバイル版でドライブのメニューを使いやすく
|
||||
* サイレンス時に確認を表示するように
|
||||
* ユーザーメニューでブロックなどの操作を行う時に確認するように
|
||||
|
||||
### Fixes
|
||||
* アプリケーション連携画面でパーミッションが表示されない問題を修正
|
||||
* アンケートウィジットでもMFMを使用するように
|
||||
* フォローしてないユーザーのホーム投稿がSTLに流れてくる問題を修正
|
||||
* モバイル版でウィジェットを設定できない問題を修正
|
||||
* スプラッシュがクリックに反応するように
|
||||
|
||||
11.1.6 (2019/04/18)
|
||||
-------------------
|
||||
### Fixes
|
||||
|
|
|
@ -35,6 +35,7 @@ common:
|
|||
signup: "新規登録"
|
||||
signout: "ログアウト"
|
||||
reload-to-apply-the-setting: "この設定を反映するにはページをリロードする必要があります。今すぐリロードしますか?"
|
||||
fetching-as-ap-object: "連合に照会中"
|
||||
|
||||
got-it: "わかった"
|
||||
customization-tips:
|
||||
|
@ -527,8 +528,12 @@ common/views/components/user-menu.vue:
|
|||
mention: "メンション"
|
||||
mute: "ミュート"
|
||||
unmute: "ミュート解除"
|
||||
mute-confirm: "このユーザーをミュートしますか?"
|
||||
unmute-confirm: "このユーザーをミュート解除しますか?"
|
||||
block: "ブロック"
|
||||
unblock: "ブロック解除"
|
||||
block-confirm: "このユーザーをブロックしますか?"
|
||||
unblock-confirm: "このユーザーをブロック解除しますか?"
|
||||
push-to-list: "リストに追加"
|
||||
select-list: "リストを選択してください"
|
||||
report-abuse: "スパムを報告"
|
||||
|
@ -536,8 +541,12 @@ common/views/components/user-menu.vue:
|
|||
report-abuse-reported: "管理者に報告されました。ご協力ありがとうございました。"
|
||||
silence: "サイレンス"
|
||||
unsilence: "サイレンス解除"
|
||||
silence-confirm: "このユーザーをサイレンスしますか?"
|
||||
unsilence-confirm: "このユーザーをサイレンス解除しますか?"
|
||||
suspend: "凍結"
|
||||
unsuspend: "凍結解除"
|
||||
suspend-confirm: "このユーザーを凍結しますか?"
|
||||
unsuspend-confirm: "このユーザーを凍結解除しますか?"
|
||||
|
||||
common/views/components/poll.vue:
|
||||
vote-to: "「{}」に投票する"
|
||||
|
@ -739,6 +748,10 @@ common/views/components/user-list-editor.vue:
|
|||
delete-are-you-sure: "リスト「$1」を削除しますか?"
|
||||
deleted: "削除しました"
|
||||
|
||||
common/views/components/user-lists.vue:
|
||||
create-list: "リストを作成"
|
||||
list-name: "リスト名"
|
||||
|
||||
common/views/widgets/broadcast.vue:
|
||||
fetching: "確認中"
|
||||
no-broadcasts: "お知らせはありません"
|
||||
|
@ -1145,8 +1158,6 @@ desktop/views/components/received-follow-requests-window.vue:
|
|||
|
||||
desktop/views/components/user-lists-window.vue:
|
||||
title: "リスト"
|
||||
create-list: "リストを作成"
|
||||
list-name: "リスト名"
|
||||
|
||||
desktop/views/components/user-preview.vue:
|
||||
notes: "投稿"
|
||||
|
@ -1336,7 +1347,9 @@ admin/views/users.vue:
|
|||
unsuspend-confirm: "凍結を解除しますか?"
|
||||
unsuspended: "凍結を解除しました"
|
||||
make-silence: "サイレンス"
|
||||
silence-confirm: "サイレンスしますか?"
|
||||
unmake-silence: "サイレンスの解除"
|
||||
unsilence-confirm: "サイレンスを解除しますか?"
|
||||
verify: "公式アカウントにする"
|
||||
verify-confirm: "公式アカウントにしますか?"
|
||||
verified: "公式アカウントにしました"
|
||||
|
@ -1573,12 +1586,11 @@ mobile/views/components/drive.vue:
|
|||
file-count: "ファイル"
|
||||
nothing-in-drive: "ドライブには何もありません"
|
||||
folder-is-empty: "このフォルダは空です"
|
||||
prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
|
||||
deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。"
|
||||
folder-name: "フォルダー名"
|
||||
here-is-root: "現在いる場所はルートで、フォルダではありません。"
|
||||
url-prompt: "アップロードしたいファイルのURL"
|
||||
uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。"
|
||||
folder-name-cannot-empty: "フォルダ名を空白にすることはできません。"
|
||||
|
||||
mobile/views/components/drive-file-chooser.vue:
|
||||
select-file: "ファイルを選択"
|
||||
|
@ -1668,9 +1680,17 @@ mobile/views/components/ui.nav.vue:
|
|||
admin: "管理"
|
||||
about: "Misskeyについて"
|
||||
|
||||
mobile/views/pages/drive.vue:
|
||||
contextmenu:
|
||||
upload: "ファイルをアップロード"
|
||||
url-upload: "ファイルをURLでアップロード"
|
||||
create-folder: "フォルダーを作成"
|
||||
rename-folder: "フォルダー名を変更"
|
||||
move-folder: "このフォルダを移動"
|
||||
delete-folder: "このフォルダを削除"
|
||||
|
||||
mobile/views/pages/user-lists.vue:
|
||||
title: "リスト"
|
||||
enter-list-name: "リスト名を入力してください"
|
||||
|
||||
mobile/views/pages/signup.vue:
|
||||
lets-start: "📦 始めましょう"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "11.1.6",
|
||||
"version": "11.2.0",
|
||||
"codename": "daybreak",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -232,6 +232,8 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
async silenceUser() {
|
||||
if (!await this.getConfirmed(this.$t('silence-confirm'))) return;
|
||||
|
||||
const process = async () => {
|
||||
await this.$root.api('admin/silence-user', { userId: this.user.id });
|
||||
this.$root.dialog({
|
||||
|
@ -251,6 +253,8 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
async unsilenceUser() {
|
||||
if (!await this.getConfirmed(this.$t('unsilence-confirm'))) return;
|
||||
|
||||
const process = async () => {
|
||||
await this.$root.api('admin/unsilence-user', { userId: this.user.id });
|
||||
this.$root.dialog({
|
||||
|
|
64
src/client/app/common/scripts/search.ts
Normal file
64
src/client/app/common/scripts/search.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { faHistory } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export async function search(v: any, q: string) {
|
||||
q = q.trim();
|
||||
|
||||
if (q.startsWith('@')) {
|
||||
v.$router.push(`/${q}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (q.startsWith('#')) {
|
||||
v.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// like 2018/03/12
|
||||
if (/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}/.test(q.replace(/-/g, '/'))) {
|
||||
const date = new Date(q.replace(/-/g, '/'));
|
||||
|
||||
// 日付しか指定されてない場合、例えば 2018/03/12 ならユーザーは
|
||||
// 2018/03/12 のコンテンツを「含む」結果になることを期待するはずなので
|
||||
// 23時間59分進める(そのままだと 2018/03/12 00:00:00 「まで」の
|
||||
// 結果になってしまい、2018/03/12 のコンテンツは含まれない)
|
||||
if (q.replace(/-/g, '/').match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}$/)) {
|
||||
date.setHours(23, 59, 59, 999);
|
||||
}
|
||||
|
||||
v.$root.$emit('warp', date);
|
||||
v.$root.dialog({
|
||||
icon: faHistory,
|
||||
splash: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (q.startsWith('https://')) {
|
||||
const dialog = v.$root.dialog({
|
||||
type: 'waiting',
|
||||
text: v.$t('@.fetching-as-ap-object'),
|
||||
showOkButton: false,
|
||||
showCancelButton: false,
|
||||
cancelableByBgClick: false
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await v.$root.api('ap/show', {
|
||||
uri: q
|
||||
});
|
||||
dialog.close();
|
||||
if (res.type == 'User') {
|
||||
v.$router.push(`/@${res.object.username}@${res.object.host}`);
|
||||
} else if (res.type == 'Note') {
|
||||
v.$router.push(`/notes/${res.object.id}`);
|
||||
}
|
||||
} catch (e) {
|
||||
dialog.close();
|
||||
// TODO: Show error
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
v.$router.push(`/search?q=${encodeURIComponent(q)}`);
|
||||
}
|
|
@ -6,7 +6,17 @@
|
|||
<mk-signin/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="icon" v-if="!input && !select && !user" :class="type"><fa :icon="icon"/></div>
|
||||
<div class="icon" v-if="icon">
|
||||
<fa :icon="icon"/>
|
||||
</div>
|
||||
<div class="icon" v-else-if="!input && !select && !user" :class="type">
|
||||
<fa icon="check" v-if="type === 'success'"/>
|
||||
<fa :icon="faTimesCircle" v-if="type === 'error'"/>
|
||||
<fa icon="exclamation-triangle" v-if="type === 'warning'"/>
|
||||
<fa icon="info-circle" v-if="type === 'info'"/>
|
||||
<fa :icon="faQuestionCircle" v-if="type === 'question'"/>
|
||||
<fa icon="spinner" pulse v-if="type === 'waiting'"/>
|
||||
</div>
|
||||
<header v-if="title" v-html="title"></header>
|
||||
<div class="body" v-if="text" v-html="text"></div>
|
||||
<ui-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></ui-input>
|
||||
|
@ -14,8 +24,8 @@
|
|||
<ui-select v-if="select" v-model="selectedValue" autofocus>
|
||||
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
|
||||
</ui-select>
|
||||
<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash">
|
||||
<ui-button @click="ok" primary :autofocus="!input && !select && !user">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button>
|
||||
<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash && (showOkButton || showCancelButton)">
|
||||
<ui-button @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button>
|
||||
<ui-button @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('@.cancel') }}</ui-button>
|
||||
</ui-horizon-group>
|
||||
</template>
|
||||
|
@ -55,10 +65,21 @@ export default Vue.extend({
|
|||
user: {
|
||||
required: false
|
||||
},
|
||||
icon: {
|
||||
required: false
|
||||
},
|
||||
showOkButton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showCancelButton: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
cancelableByBgClick: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
splash: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
|
@ -69,22 +90,11 @@ export default Vue.extend({
|
|||
return {
|
||||
inputValue: this.input && this.input.default ? this.input.default : null,
|
||||
userInputValue: null,
|
||||
selectedValue: null
|
||||
selectedValue: null,
|
||||
faTimesCircle, faQuestionCircle
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
icon(): any {
|
||||
switch (this.type) {
|
||||
case 'success': return 'check';
|
||||
case 'error': return faTimesCircle;
|
||||
case 'warning': return 'exclamation-triangle';
|
||||
case 'info': return 'info-circle';
|
||||
case 'question': return faQuestionCircle;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
(this.$refs.bg as any).style.pointerEvents = 'auto';
|
||||
|
@ -113,6 +123,8 @@ export default Vue.extend({
|
|||
|
||||
methods: {
|
||||
async ok() {
|
||||
if (!this.showOkButton) return;
|
||||
|
||||
if (this.user) {
|
||||
const user = await this.$root.api('users/show', parseAcct(this.userInputValue));
|
||||
if (user) {
|
||||
|
@ -156,7 +168,9 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
onBgClick() {
|
||||
if (this.cancelableByBgClick) {
|
||||
this.cancel();
|
||||
}
|
||||
},
|
||||
|
||||
onInputKeydown(e) {
|
||||
|
@ -183,9 +197,6 @@ export default Vue.extend({
|
|||
height 100%
|
||||
|
||||
&.splash
|
||||
&, *
|
||||
pointer-events none !important
|
||||
|
||||
> .main
|
||||
min-width 0
|
||||
width initial
|
||||
|
@ -243,7 +254,7 @@ export default Vue.extend({
|
|||
margin-top 8px
|
||||
|
||||
> .body
|
||||
margin 16px 0
|
||||
margin 16px 0 0 0
|
||||
|
||||
> .buttons
|
||||
margin-top 16px
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<div class="body">
|
||||
<p class="init" v-if="init"><fa icon="spinner .spin"/>{{ $t('@.loading') }}</p>
|
||||
<p class="empty" v-if="!init && messages.length == 0"><fa icon="info-circle"/>{{ $t('empty') }}</p>
|
||||
<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages"><fa icon="flag"/>{{ $t('no-history') }}</p>
|
||||
<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages"><fa :icon="faFlag"/>{{ $t('no-history') }}</p>
|
||||
<button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
|
||||
<template v-if="fetchingMoreMessages"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreMessages ? $t('@.loading') : $t('@.load-more') }}
|
||||
</button>
|
||||
|
@ -35,6 +35,7 @@ import XMessage from './messaging-room.message.vue';
|
|||
import XForm from './messaging-room.form.vue';
|
||||
import { url } from '../../../config';
|
||||
import { faArrowCircleDown } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faFlag } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/messaging-room.vue'),
|
||||
|
@ -54,7 +55,7 @@ export default Vue.extend({
|
|||
connection: null,
|
||||
showIndicator: false,
|
||||
timer: null,
|
||||
faArrowCircleDown
|
||||
faArrowCircleDown, faFlag
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
<section>
|
||||
<header><fa icon="terminal"/> {{ $t('console.title') }}</header>
|
||||
<ui-input v-model="endpoint">
|
||||
<ui-input v-model="endpoint" :datalist="endpoints">
|
||||
<span>{{ $t('console.endpoint') }}</span>
|
||||
</ui-input>
|
||||
<ui-textarea v-model="body">
|
||||
|
@ -39,15 +39,23 @@ import * as JSON5 from 'json5';
|
|||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/api-settings.vue'),
|
||||
|
||||
data() {
|
||||
return {
|
||||
endpoint: '',
|
||||
body: '{}',
|
||||
res: null,
|
||||
sending: false
|
||||
sending: false,
|
||||
endpoints: []
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$root.api('endpoints').then(endpoints => {
|
||||
this.endpoints = endpoints;
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
regenerateToken() {
|
||||
this.$root.dialog({
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@keydown="$emit('keydown', $event)"
|
||||
:list="id"
|
||||
>
|
||||
<input v-else ref="input"
|
||||
:type="type"
|
||||
|
@ -37,7 +38,11 @@
|
|||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@keydown="$emit('keydown', $event)"
|
||||
:list="id"
|
||||
>
|
||||
<datalist :id="id" v-if="datalist">
|
||||
<option v-for="data in datalist" :value="data"/>
|
||||
</datalist>
|
||||
</template>
|
||||
<template v-else>
|
||||
<input ref="input"
|
||||
|
@ -130,6 +135,10 @@ export default Vue.extend({
|
|||
required: false,
|
||||
default: false
|
||||
},
|
||||
datalist: {
|
||||
type: Array,
|
||||
required: false,
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -147,7 +156,8 @@ export default Vue.extend({
|
|||
return {
|
||||
v: this.value,
|
||||
focused: false,
|
||||
passwordStrength: ''
|
||||
passwordStrength: '',
|
||||
id: Math.random().toString()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
95
src/client/app/common/views/components/user-lists.vue
Normal file
95
src/client/app/common/views/components/user-lists.vue
Normal file
|
@ -0,0 +1,95 @@
|
|||
<template>
|
||||
<div class="xkxvokkjlptzyewouewmceqcxhpgzprp">
|
||||
<button class="ui" @click="add">{{ $t('create-list') }}</button>
|
||||
<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.name }}</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/user-lists.vue'),
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
lists: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$root.api('users/lists/list').then(lists => {
|
||||
this.fetching = false;
|
||||
this.lists = lists;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
add() {
|
||||
this.$root.dialog({
|
||||
title: this.$t('list-name'),
|
||||
input: true
|
||||
}).then(async ({ canceled, result: title }) => {
|
||||
if (canceled) return;
|
||||
const list = await this.$root.api('users/lists/create', {
|
||||
title
|
||||
});
|
||||
|
||||
this.lists.push(list)
|
||||
this.$emit('choosen', list);
|
||||
});
|
||||
},
|
||||
choice(list) {
|
||||
this.$emit('choosen', list);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.xkxvokkjlptzyewouewmceqcxhpgzprp
|
||||
padding 16px
|
||||
background: var(--bg)
|
||||
|
||||
> button
|
||||
display block
|
||||
margin-bottom 16px
|
||||
color var(--primaryForeground)
|
||||
background var(--primary)
|
||||
width 100%
|
||||
border-radius 38px
|
||||
user-select none
|
||||
cursor pointer
|
||||
padding 0 16px
|
||||
min-width 100px
|
||||
line-height 38px
|
||||
font-size 14px
|
||||
font-weight 700
|
||||
|
||||
&:hover
|
||||
background var(--primaryLighten10)
|
||||
|
||||
&:active
|
||||
background var(--primaryDarken10)
|
||||
|
||||
a
|
||||
display block
|
||||
margin 8px 0
|
||||
padding 8px
|
||||
color var(--text)
|
||||
background var(--face)
|
||||
box-shadow 0 2px 16px var(--reversiListItemShadow)
|
||||
border-radius 6px
|
||||
cursor pointer
|
||||
line-height 32px
|
||||
|
||||
*
|
||||
pointer-events none
|
||||
user-select none
|
||||
|
||||
&:hover
|
||||
box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
|
||||
|
||||
&:active
|
||||
box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
|
||||
|
||||
</style>
|
|
@ -89,8 +89,10 @@ export default Vue.extend({
|
|||
});
|
||||
},
|
||||
|
||||
toggleMute() {
|
||||
async toggleMute() {
|
||||
if (this.user.isMuted) {
|
||||
if (!await this.getConfirmed(this.$t('unmute-confirm'))) return;
|
||||
|
||||
this.$root.api('mute/delete', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
|
@ -102,6 +104,8 @@ export default Vue.extend({
|
|||
});
|
||||
});
|
||||
} else {
|
||||
if (!await this.getConfirmed(this.$t('mute-confirm'))) return;
|
||||
|
||||
this.$root.api('mute/create', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
|
@ -115,8 +119,10 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
|
||||
toggleBlock() {
|
||||
async toggleBlock() {
|
||||
if (this.user.isBlocking) {
|
||||
if (!await this.getConfirmed(this.$t('unblock-confirm'))) return;
|
||||
|
||||
this.$root.api('blocking/delete', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
|
@ -128,6 +134,8 @@ export default Vue.extend({
|
|||
});
|
||||
});
|
||||
} else {
|
||||
if (!await this.getConfirmed(this.$t('block-confirm'))) return;
|
||||
|
||||
this.$root.api('blocking/create', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
|
@ -164,7 +172,9 @@ export default Vue.extend({
|
|||
});
|
||||
},
|
||||
|
||||
toggleSilence() {
|
||||
async toggleSilence() {
|
||||
if (!await this.getConfirmed(this.$t(this.user.isSilenced ? 'unsilence-confirm' : 'silence-confirm'))) return;
|
||||
|
||||
this.$root.api(this.user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
|
@ -181,7 +191,9 @@ export default Vue.extend({
|
|||
});
|
||||
},
|
||||
|
||||
toggleSuspend() {
|
||||
async toggleSuspend() {
|
||||
if (!await this.getConfirmed(this.$t(this.user.isSuspended ? 'unsuspend-confirm' : 'suspend-confirm'))) return;
|
||||
|
||||
this.$root.api(this.user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
|
@ -196,7 +208,18 @@ export default Vue.extend({
|
|||
text: e
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async getConfirmed(text: string): Promise<Boolean> {
|
||||
const confirm = await this.$root.dialog({
|
||||
type: 'warning',
|
||||
showCancelButton: true,
|
||||
title: 'confirm',
|
||||
text,
|
||||
});
|
||||
|
||||
return !confirm.canceled;
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -123,7 +123,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
fetchMore() {
|
||||
if (!this.more || this.moreFetching) return;
|
||||
if (!this.more || this.moreFetching || this.notes.length === 0) return;
|
||||
this.moreFetching = true;
|
||||
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
|
||||
this.notes = this.notes.concat(x.notes);
|
||||
|
|
|
@ -90,9 +90,8 @@
|
|||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import MkUserListsWindow from './user-lists-window.vue';
|
||||
import MkUserListWindow from './user-list-window.vue';
|
||||
import MkFollowRequestsWindow from './received-follow-requests-window.vue';
|
||||
import MkSettingsWindow from './settings-window.vue';
|
||||
// import MkSettingsWindow from './settings-window.vue';
|
||||
import MkDriveWindow from './drive-window.vue';
|
||||
import contains from '../../../common/scripts/contains';
|
||||
import { faHome, faColumns } from '@fortawesome/free-solid-svg-icons';
|
||||
|
@ -143,12 +142,7 @@ export default Vue.extend({
|
|||
},
|
||||
list() {
|
||||
this.close();
|
||||
const w = this.$root.new(MkUserListsWindow);
|
||||
w.$once('choosen', list => {
|
||||
this.$root.new(MkUserListWindow, {
|
||||
list
|
||||
});
|
||||
});
|
||||
this.$root.new(MkUserListsWindow);
|
||||
},
|
||||
followRequests() {
|
||||
this.close();
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import { search } from '../../../common/scripts/search';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('desktop/views/components/ui.header.search.vue'),
|
||||
|
@ -22,29 +23,11 @@ export default Vue.extend({
|
|||
async onSubmit() {
|
||||
if (this.wait) return;
|
||||
|
||||
const q = this.q.trim();
|
||||
if (q.startsWith('@')) {
|
||||
this.$router.push(`/${q}`);
|
||||
} else if (q.startsWith('#')) {
|
||||
this.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
|
||||
} else if (q.startsWith('https://')) {
|
||||
this.wait = true;
|
||||
try {
|
||||
const res = await this.$root.api('ap/show', {
|
||||
uri: q
|
||||
});
|
||||
if (res.type == 'User') {
|
||||
this.$router.push(`/@${res.object.username}@${res.object.host}`);
|
||||
} else if (res.type == 'Note') {
|
||||
this.$router.push(`/notes/${res.object.id}`);
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO
|
||||
}
|
||||
search(this, this.q).finally(() => {
|
||||
this.wait = false;
|
||||
} else {
|
||||
this.$router.push(`/search?q=${encodeURIComponent(q)}`);
|
||||
}
|
||||
this.q = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -148,10 +148,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
list() {
|
||||
const w = this.$root.new(MkUserListsWindow);
|
||||
w.$once('choosen', list => {
|
||||
this.$router.push(`i/lists/${ list.id }`);
|
||||
});
|
||||
this.$root.new(MkUserListsWindow);
|
||||
},
|
||||
|
||||
followRequests() {
|
||||
|
|
|
@ -18,10 +18,12 @@ export default Vue.extend({
|
|||
data() {
|
||||
return {
|
||||
connection: null,
|
||||
date: null,
|
||||
makePromise: cursor => this.$root.api('notes/user-list-timeline', {
|
||||
listId: this.list.id,
|
||||
limit: fetchLimit + 1,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
|
@ -46,6 +48,10 @@ export default Vue.extend({
|
|||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
this.$root.$on('warp', this.warp);
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
this.$root.$off('warp', this.warp);
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.connection.dispose();
|
||||
|
@ -68,6 +74,10 @@ export default Vue.extend({
|
|||
},
|
||||
onUserRemoved() {
|
||||
(this.$refs.timeline as any).reload();
|
||||
},
|
||||
warp(date) {
|
||||
this.date = date;
|
||||
(this.$refs.timeline as any).reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,85 +1,36 @@
|
|||
<template>
|
||||
<mk-window ref="window" width="450px" height="500px" @closed="destroyDom">
|
||||
<template #header><fa icon="list"/> {{ $t('title') }}</template>
|
||||
|
||||
<div class="xkxvokkjlptzyewouewmceqcxhpgzprp">
|
||||
<button class="ui" @click="add">{{ $t('create-list') }}</button>
|
||||
<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.name }}</a>
|
||||
</div>
|
||||
<x-lists :class="$style.content" @choosen="choosen"/>
|
||||
</mk-window>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import MkUserListWindow from './user-list-window.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('desktop/views/components/user-lists-window.vue'),
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
lists: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$root.api('users/lists/list').then(lists => {
|
||||
this.fetching = false;
|
||||
this.lists = lists;
|
||||
});
|
||||
components: {
|
||||
XLists: () => import('../../../common/views/components/user-lists.vue').then(m => m.default)
|
||||
},
|
||||
methods: {
|
||||
add() {
|
||||
this.$root.dialog({
|
||||
title: this.$t('list-name'),
|
||||
input: true
|
||||
}).then(async ({ canceled, result: title }) => {
|
||||
if (canceled) return;
|
||||
const list = await this.$root.api('users/lists/create', {
|
||||
title
|
||||
});
|
||||
|
||||
this.$emit('choosen', list);
|
||||
});
|
||||
},
|
||||
choice(list) {
|
||||
this.$emit('choosen', list);
|
||||
},
|
||||
close() {
|
||||
(this as any).$refs.window.close();
|
||||
},
|
||||
choosen(list) {
|
||||
this.$root.new(MkUserListWindow, {
|
||||
list
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.xkxvokkjlptzyewouewmceqcxhpgzprp
|
||||
padding 16px
|
||||
|
||||
> button
|
||||
display block
|
||||
margin-bottom 16px
|
||||
color var(--primaryForeground)
|
||||
background var(--primary)
|
||||
width 100%
|
||||
border-radius 38px
|
||||
user-select none
|
||||
cursor pointer
|
||||
padding 0 16px
|
||||
min-width 100px
|
||||
line-height 38px
|
||||
font-size 14px
|
||||
font-weight 700
|
||||
|
||||
&:hover
|
||||
background var(--primaryLighten10)
|
||||
|
||||
&:active
|
||||
background var(--primaryDarken10)
|
||||
|
||||
> a
|
||||
display block
|
||||
padding 16px
|
||||
border solid 1px var(--faceDivider)
|
||||
border-radius 4px
|
||||
<style lang="stylus" module>
|
||||
.content
|
||||
height 100%
|
||||
overflow auto
|
||||
|
||||
</style>
|
||||
|
|
|
@ -53,6 +53,12 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
created() {
|
||||
this.$root.$on('warp', this.warp);
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
this.$root.$off('warp', this.warp);
|
||||
this.connection.dispose();
|
||||
});
|
||||
|
||||
const prepend = note => {
|
||||
(this.$refs.timeline as any).prepend(note);
|
||||
};
|
||||
|
@ -124,13 +130,14 @@ export default Vue.extend({
|
|||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.dispose();
|
||||
},
|
||||
|
||||
methods: {
|
||||
focus() {
|
||||
(this.$refs.timeline as any).focus();
|
||||
},
|
||||
|
||||
warp(date) {
|
||||
this.date = date;
|
||||
(this.$refs.timeline as any).reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -36,7 +36,8 @@ export default Vue.extend({
|
|||
includeReplies: this.mode == 'with-replies',
|
||||
includeMyRenotes: this.mode != 'my-posts',
|
||||
withFiles: this.mode == 'with-media',
|
||||
untilDate: cursor ? cursor : new Date().getTime() + 1000 * 86400 * 365
|
||||
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
untilId: cursor ? cursor : undefined
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
|
@ -62,10 +63,11 @@ export default Vue.extend({
|
|||
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.onDocumentKeydown);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.$root.$on('warp', this.warp);
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
this.$root.$off('warp', this.warp);
|
||||
document.removeEventListener('keydown', this.onDocumentKeydown);
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
|
@ -11,7 +11,9 @@
|
|||
|
||||
<div class="mkw-polls--body">
|
||||
<div class="poll" v-if="!fetching && poll != null">
|
||||
<p v-if="poll.text"><router-link :to="poll | notePage">{{ poll.text }}</router-link></p>
|
||||
<p v-if="poll.text"><router-link :to="poll | notePage">
|
||||
<mfm :text="poll.text" :author="poll.user" :custom-emojis="poll.emojis"/>
|
||||
</router-link></p>
|
||||
<p v-if="!poll.text"><router-link :to="poll | notePage"><fa icon="link"/></router-link></p>
|
||||
<mk-poll :note="poll"/>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@ export default define({
|
|||
}).extend({
|
||||
methods: {
|
||||
chosen(date) {
|
||||
this.$emit('chosen', date);
|
||||
this.$root.$emit('warp', date);
|
||||
},
|
||||
func() {
|
||||
if (this.props.design == 5) {
|
||||
|
|
|
@ -458,10 +458,14 @@ export default (callback: (launch: (router: VueRouter) => [Vue, MiOS], os: MiOS)
|
|||
},
|
||||
dialog(opts) {
|
||||
const vm = this.new(Dialog, opts);
|
||||
return new Promise((res) => {
|
||||
const p: any = new Promise((res) => {
|
||||
vm.$once('ok', result => res({ canceled: false, result }));
|
||||
vm.$once('cancel', () => res({ canceled: true }));
|
||||
});
|
||||
p.close = () => {
|
||||
vm.close();
|
||||
};
|
||||
return p;
|
||||
}
|
||||
},
|
||||
router,
|
||||
|
|
|
@ -379,44 +379,31 @@ export default Vue.extend({
|
|||
});
|
||||
},
|
||||
|
||||
openContextMenu() {
|
||||
const fn = window.prompt(this.$t('prompt'));
|
||||
if (fn == null || fn == '') return;
|
||||
switch (fn) {
|
||||
case '1':
|
||||
this.selectLocalFile();
|
||||
break;
|
||||
case '2':
|
||||
this.urlUpload();
|
||||
break;
|
||||
case '3':
|
||||
this.createFolder();
|
||||
break;
|
||||
case '4':
|
||||
this.renameFolder();
|
||||
break;
|
||||
case '5':
|
||||
this.moveFolder();
|
||||
break;
|
||||
case '6':
|
||||
this.deleteFolder();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
selectLocalFile() {
|
||||
(this.$refs.file as any).click();
|
||||
},
|
||||
|
||||
createFolder() {
|
||||
const name = window.prompt(this.$t('folder-name'));
|
||||
if (name == null || name == '') return;
|
||||
this.$root.dialog({
|
||||
title: this.$t('folder-name')
|
||||
input: {
|
||||
default: this.folder.name
|
||||
}
|
||||
}).then(({ result: name }) => {
|
||||
if (!name) {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('folder-name-cannot-empty')
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.$root.api('drive/folders/create', {
|
||||
name: name,
|
||||
parentId: this.folder ? this.folder.id : undefined
|
||||
}).then(folder => {
|
||||
this.addFolder(folder, true);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
renameFolder() {
|
||||
|
@ -427,14 +414,26 @@ export default Vue.extend({
|
|||
});
|
||||
return;
|
||||
}
|
||||
const name = window.prompt(this.$t('folder-name'), this.folder.name);
|
||||
if (name == null || name == '') return;
|
||||
this.$root.dialog({
|
||||
title: this.$t('folder-name')
|
||||
input: {
|
||||
default: this.folder.name
|
||||
}
|
||||
}).then(({ result: name }) => {
|
||||
if (!name) {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('cannot-empty')
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.$root.api('drive/folders/update', {
|
||||
name: name,
|
||||
folderId: this.folder.id
|
||||
}).then(folder => {
|
||||
this.cd(folder);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
moveFolder() {
|
||||
|
|
|
@ -124,7 +124,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
fetchMore() {
|
||||
if (!this.more || this.moreFetching) return;
|
||||
if (!this.more || this.moreFetching || this.notes.length === 0) return;
|
||||
this.moreFetching = true;
|
||||
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
|
||||
this.notes = this.notes.concat(x.notes);
|
||||
|
|
|
@ -66,6 +66,7 @@ import i18n from '../../../i18n';
|
|||
import { lang } from '../../../config';
|
||||
import { faNewspaper, faHashtag, faHome, faColumns } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
|
||||
import { search } from '../../../common/scripts/search';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('mobile/views/components/ui.nav.vue'),
|
||||
|
@ -133,29 +134,10 @@ export default Vue.extend({
|
|||
}).then(async ({ canceled, result: query }) => {
|
||||
if (canceled) return;
|
||||
|
||||
const q = query.trim();
|
||||
if (q.startsWith('@')) {
|
||||
this.$router.push(`/${q}`);
|
||||
} else if (q.startsWith('#')) {
|
||||
this.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
|
||||
} else if (q.startsWith('https://')) {
|
||||
this.searching = true;
|
||||
try {
|
||||
const res = await this.$root.api('ap/show', {
|
||||
uri: q
|
||||
});
|
||||
if (res.type == 'User') {
|
||||
this.$router.push(`/@${res.object.username}@${res.object.host}`);
|
||||
} else if (res.type == 'Note') {
|
||||
this.$router.push(`/notes/${res.object.id}`);
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO
|
||||
}
|
||||
search(this, query).finally(() => {
|
||||
this.searching = false;
|
||||
} else {
|
||||
this.$router.push(`/search?q=${encodeURIComponent(q)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -15,10 +15,12 @@ export default Vue.extend({
|
|||
data() {
|
||||
return {
|
||||
connection: null,
|
||||
date: null,
|
||||
makePromise: cursor => this.$root.api('notes/user-list-timeline', {
|
||||
listId: this.list.id,
|
||||
limit: fetchLimit + 1,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
|
||||
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
|
||||
|
@ -45,6 +47,11 @@ export default Vue.extend({
|
|||
|
||||
mounted() {
|
||||
this.init();
|
||||
|
||||
this.$root.$on('warp', this.warp);
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
this.$root.$off('warp', this.warp);
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
|
@ -73,6 +80,11 @@ export default Vue.extend({
|
|||
|
||||
onUserRemoved() {
|
||||
(this.$refs.timeline as any).reload();
|
||||
},
|
||||
|
||||
warp(date) {
|
||||
this.date = date;
|
||||
(this.$refs.timeline as any).reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -17,11 +17,13 @@ export default Vue.extend({
|
|||
|
||||
data() {
|
||||
return {
|
||||
date: null,
|
||||
makePromise: cursor => this.$root.api('users/notes', {
|
||||
userId: this.user.id,
|
||||
limit: fetchLimit + 1,
|
||||
withFiles: this.withMedia,
|
||||
untilDate: cursor ? cursor : new Date().getTime() + 1000 * 86400 * 365
|
||||
untilDate: cursor ? undefined : (this.date ? this.date.getTime() : undefined),
|
||||
untilId: cursor ? cursor : undefined
|
||||
}).then(notes => {
|
||||
if (notes.length == fetchLimit + 1) {
|
||||
notes.pop();
|
||||
|
@ -37,6 +39,20 @@ export default Vue.extend({
|
|||
}
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$root.$on('warp', this.warp);
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
this.$root.$off('warp', this.warp);
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
warp(date) {
|
||||
this.date = date;
|
||||
(this.$refs.timeline as any).reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<template v-if="file"><mk-file-type-icon data-icon :type="file.type" style="margin-right:4px;"/>{{ file.name }}</template>
|
||||
<template v-if="!folder && !file"><span style="margin-right:4px;"><fa icon="cloud"/></span>{{ $t('@.drive') }}</template>
|
||||
</template>
|
||||
<template #func><button @click="fn"><fa icon="ellipsis-h"/></button></template>
|
||||
<template #func v-if="folder || (!folder && !file)"><button @click="openContextMenu" ref="contextSource"><fa icon="ellipsis-h"/></button></template>
|
||||
<x-drive
|
||||
ref="browser"
|
||||
:init-folder="initFolder"
|
||||
|
@ -26,9 +26,12 @@
|
|||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
import XMenu from '../../../common/views/components/menu.vue';
|
||||
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
i18n: i18n('mobile/views/pages/drive.vue'),
|
||||
components: {
|
||||
XDrive: () => import('../components/drive.vue').then(m => m.default),
|
||||
},
|
||||
|
@ -63,9 +66,6 @@ export default Vue.extend({
|
|||
(this.$refs as any).browser.goRoot(true);
|
||||
}
|
||||
},
|
||||
fn() {
|
||||
(this.$refs as any).browser.openContextMenu();
|
||||
},
|
||||
onMoveRoot(silent) {
|
||||
const title = `${this.$root.instanceName} Drive`;
|
||||
|
||||
|
@ -104,6 +104,42 @@ export default Vue.extend({
|
|||
|
||||
this.file = file;
|
||||
this.folder = null;
|
||||
},
|
||||
openContextMenu() {
|
||||
this.$root.new(XMenu, {
|
||||
items: [{
|
||||
type: 'item',
|
||||
text: this.$t('contextmenu.upload'),
|
||||
icon: 'upload',
|
||||
action: this.$refs.browser.selectLocalFile
|
||||
}, {
|
||||
type: 'item',
|
||||
text: this.$t('contextmenu.url-upload'),
|
||||
icon: faCloudUploadAlt,
|
||||
action: this.$refs.browser.urlUpload
|
||||
}, {
|
||||
type: 'item',
|
||||
text: this.$t('contextmenu.create-folder'),
|
||||
icon: ['far', 'folder'],
|
||||
action: this.$refs.browser.createFolder
|
||||
}, ...(this.folder ? [{
|
||||
type: 'item',
|
||||
text: this.$t('contextmenu.rename-folder'),
|
||||
icon: 'i-cursor',
|
||||
action: this.$refs.browser.renameFolder
|
||||
}, {
|
||||
type: 'item',
|
||||
text: this.$t('contextmenu.move-folder'),
|
||||
icon: ['far', 'folder-open'],
|
||||
action: this.$refs.browser.moveFolder
|
||||
}, {
|
||||
type: 'item',
|
||||
text: this.$t('contextmenu.delete-folder'),
|
||||
icon: faTrashAlt,
|
||||
action: this.$refs.browser.deleteFolder
|
||||
}] : [])],
|
||||
source: this.$refs.contextSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -54,6 +54,12 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
created() {
|
||||
this.$root.$on('warp', this.warp);
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
this.$root.$off('warp', this.warp);
|
||||
this.connection.dispose();
|
||||
});
|
||||
|
||||
const prepend = note => {
|
||||
(this.$refs.timeline as any).prepend(note);
|
||||
};
|
||||
|
@ -125,10 +131,6 @@ export default Vue.extend({
|
|||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.dispose();
|
||||
},
|
||||
|
||||
methods: {
|
||||
focus() {
|
||||
(this.$refs.timeline as any).focus();
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
<template>
|
||||
<mk-ui>
|
||||
<template #header><fa icon="list"/>{{ $t('title') }}</template>
|
||||
<template #func><button @click="fn"><fa icon="plus"/></button></template>
|
||||
<template #func><button @click="$refs.lists.add()"><fa icon="plus"/></button></template>
|
||||
|
||||
<main>
|
||||
<ul>
|
||||
<li v-for="list in lists" :key="list.id"><router-link :to="`/i/lists/${list.id}`">{{ list.name }}</router-link></li>
|
||||
</ul>
|
||||
</main>
|
||||
<x-lists ref="lists" @choosen="choosen"/>
|
||||
</mk-ui>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('mobile/views/pages/user-lists.vue'),
|
||||
|
@ -24,31 +19,16 @@ export default Vue.extend({
|
|||
lists: []
|
||||
};
|
||||
},
|
||||
components: {
|
||||
XLists: () => import('../../../common/views/components/user-lists.vue').then(m => m.default)
|
||||
},
|
||||
mounted() {
|
||||
document.title = this.$t('title');
|
||||
|
||||
Progress.start();
|
||||
|
||||
this.$root.api('users/lists/list').then(lists => {
|
||||
this.fetching = false;
|
||||
this.lists = lists;
|
||||
|
||||
Progress.done();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
fn() {
|
||||
this.$root.dialog({
|
||||
title: this.$t('enter-list-name'),
|
||||
input: true
|
||||
}).then(async ({ canceled, result: title }) => {
|
||||
if (canceled) return;
|
||||
const list = await this.$root.api('users/lists/create', {
|
||||
title
|
||||
});
|
||||
|
||||
choosen(list) {
|
||||
if (!list) return;
|
||||
this.$router.push(`/i/lists/${list.id}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -72,13 +72,13 @@ export default Vue.extend({
|
|||
|
||||
computed: {
|
||||
widgets(): any[] {
|
||||
return this.$store.state.settings.mobileHome;
|
||||
return this.$store.state.device.mobileHome;
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.widgets.length == 0) {
|
||||
this.widgets = [{
|
||||
this.$store.commit('device/setMobileHome', [{
|
||||
name: 'calendar',
|
||||
id: 'a', data: {}
|
||||
}, {
|
||||
|
@ -96,8 +96,7 @@ export default Vue.extend({
|
|||
}, {
|
||||
name: 'version',
|
||||
id: 'g', data: {}
|
||||
}];
|
||||
this.saveHome();
|
||||
}]);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -123,7 +122,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
addWidget() {
|
||||
this.$store.commit('settings/addMobileHomeWidget', {
|
||||
this.$store.commit('device/addMobileHomeWidget', {
|
||||
name: this.widgetAdderSelected,
|
||||
id: uuid(),
|
||||
data: {}
|
||||
|
@ -131,11 +130,11 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
removeWidget(widget) {
|
||||
this.$store.commit('settings/removeMobileHomeWidget', widget);
|
||||
this.$store.commit('device/removeMobileHomeWidget', widget);
|
||||
},
|
||||
|
||||
saveHome() {
|
||||
this.$store.commit('settings/setMobileHome', this.widgets);
|
||||
this.$store.commit('device/setMobileHome', this.widgets);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -358,7 +358,7 @@ export default (os: MiOS) => new Vuex.Store({
|
|||
ctx.commit('set', x);
|
||||
|
||||
if (ctx.rootGetters.isSignedIn) {
|
||||
os.api('i/update_client_setting', {
|
||||
os.api('i/update-client-setting', {
|
||||
name: x.key,
|
||||
value: x.value
|
||||
});
|
||||
|
|
|
@ -26,6 +26,7 @@ export class AppRepository extends Repository<App> {
|
|||
id: app.id,
|
||||
name: app.name,
|
||||
callbackUrl: app.callbackUrl,
|
||||
permission: app.permission,
|
||||
...(opts.includeSecret ? { secret: app.secret } : {}),
|
||||
...(me ? {
|
||||
isAuthorized: await AccessTokens.count({
|
||||
|
|
15
src/server/api/endpoints/endpoints.ts
Normal file
15
src/server/api/endpoints/endpoints.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import define from '../define';
|
||||
import endpoints from '../endpoints';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
|
||||
tags: ['meta'],
|
||||
|
||||
params: {
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async () => {
|
||||
return endpoints.map(x => x.name);
|
||||
});
|
|
@ -17,6 +17,8 @@ export const meta = {
|
|||
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
|
|
|
@ -142,7 +142,7 @@ export default define(meta, async (ps, me) => {
|
|||
});
|
||||
|
||||
//#region Construct query
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('note.userId = :userId', { userId: user.id })
|
||||
.leftJoinAndSelect('note.user', 'user');
|
||||
|
||||
|
|
|
@ -20,11 +20,11 @@ export default class extends Channel {
|
|||
|
||||
@autobind
|
||||
private async onNote(note: any) {
|
||||
// 自分自身の投稿 または その投稿のユーザーをフォローしている または ローカルの投稿 の場合だけ
|
||||
// 自分自身の投稿 または その投稿のユーザーをフォローしている または 全体公開のローカルの投稿 の場合だけ
|
||||
if (!(
|
||||
this.user!.id === note.userId ||
|
||||
this.following.includes(note.userId) ||
|
||||
note.user.host == null
|
||||
(note.user.host == null && note.visibility === 'public')
|
||||
)) return;
|
||||
|
||||
if (['followers', 'specified'].includes(note.visibility)) {
|
||||
|
|
|
@ -32,7 +32,7 @@ describe('Streaming', () => {
|
|||
p.on('message', message => {
|
||||
if (message === 'ok') {
|
||||
(p.channel as any).onread = () => {};
|
||||
initDb(true).then(async connection => {
|
||||
initDb(true).then(async (connection: any) => {
|
||||
Followings = connection.getRepository(Following);
|
||||
done();
|
||||
});
|
||||
|
@ -44,7 +44,7 @@ describe('Streaming', () => {
|
|||
p.kill();
|
||||
});
|
||||
|
||||
const follow = async (follower, followee) => {
|
||||
const follow = async (follower: any, followee: any) => {
|
||||
await Followings.save({
|
||||
id: 'a',
|
||||
createdAt: new Date(),
|
||||
|
@ -484,6 +484,56 @@ describe('Streaming', () => {
|
|||
});
|
||||
}));
|
||||
|
||||
it('フォローしているユーザーのホーム投稿が流れる', () => new Promise(async done => {
|
||||
const alice = await signup({ username: 'alice' });
|
||||
const bob = await signup({ username: 'bob' });
|
||||
|
||||
// Alice が Bob をフォロー
|
||||
await request('/following/create', {
|
||||
userId: bob.id
|
||||
}, alice);
|
||||
|
||||
const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
|
||||
if (type == 'note') {
|
||||
assert.deepStrictEqual(body.userId, bob.id);
|
||||
assert.deepStrictEqual(body.text, 'foo');
|
||||
ws.close();
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
// ホーム投稿
|
||||
post(bob, {
|
||||
text: 'foo',
|
||||
visibility: 'home'
|
||||
});
|
||||
}));
|
||||
|
||||
it('フォローしていないローカルユーザーのホーム投稿は流れない', () => new Promise(async done => {
|
||||
const alice = await signup({ username: 'alice' });
|
||||
const bob = await signup({ username: 'bob' });
|
||||
|
||||
let fired = false;
|
||||
|
||||
const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
|
||||
if (type == 'note') {
|
||||
fired = true;
|
||||
}
|
||||
});
|
||||
|
||||
// ホーム投稿
|
||||
post(bob, {
|
||||
text: 'foo',
|
||||
visibility: 'home'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(fired, false);
|
||||
ws.close();
|
||||
done();
|
||||
}, 3000);
|
||||
}));
|
||||
|
||||
it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
|
||||
const alice = await signup({ username: 'alice' });
|
||||
const bob = await signup({ username: 'bob' });
|
||||
|
|
|
@ -76,7 +76,7 @@ export const uploadFile = (user: any, path?: string): Promise<any> => new Promis
|
|||
});
|
||||
});
|
||||
|
||||
export function connectStream(user: any, channel: string, listener: any, params?: any): Promise<WebSocket> {
|
||||
export function connectStream(user: any, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
|
||||
return new Promise((res, rej) => {
|
||||
const ws = new WebSocket(`ws://localhost/streaming?i=${user.token}`);
|
||||
|
||||
|
|
Loading…
Reference in a new issue