mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-30 12:53:09 +02:00
commit
e25e1d88d6
48 changed files with 1071 additions and 213 deletions
|
@ -331,8 +331,10 @@ desktop/views/components/drive.vue:
|
||||||
url-upload: "URLからアップロード"
|
url-upload: "URLからアップロード"
|
||||||
|
|
||||||
desktop/views/components/follow-button.vue:
|
desktop/views/components/follow-button.vue:
|
||||||
unfollow: "フォロー解除"
|
unfollow: "フォロー中"
|
||||||
follow: "フォローする"
|
follow: "フォロー"
|
||||||
|
request-pending: "フォロー許可待ち"
|
||||||
|
follow-request: "フォロー申請"
|
||||||
|
|
||||||
desktop/views/components/followers-window.vue:
|
desktop/views/components/followers-window.vue:
|
||||||
followers: "{} のフォロワー"
|
followers: "{} のフォロワー"
|
||||||
|
@ -596,6 +598,7 @@ desktop/views/components/ui.header.account.vue:
|
||||||
drive: "ドライブ"
|
drive: "ドライブ"
|
||||||
favorites: "お気に入り"
|
favorites: "お気に入り"
|
||||||
lists: "リスト"
|
lists: "リスト"
|
||||||
|
follow-requests: "フォロー申請"
|
||||||
customize: "カスタマイズ"
|
customize: "カスタマイズ"
|
||||||
settings: "設定"
|
settings: "設定"
|
||||||
signout: "サインアウト"
|
signout: "サインアウト"
|
||||||
|
@ -615,7 +618,13 @@ desktop/views/components/ui.header.post.vue:
|
||||||
desktop/views/components/ui.header.search.vue:
|
desktop/views/components/ui.header.search.vue:
|
||||||
placeholder: "検索"
|
placeholder: "検索"
|
||||||
|
|
||||||
|
desktop/views/components/received-follow-requests-window.vue:
|
||||||
|
title: "フォロー申請"
|
||||||
|
accept: "承認"
|
||||||
|
reject: "拒否"
|
||||||
|
|
||||||
desktop/views/components/user-lists-window.vue:
|
desktop/views/components/user-lists-window.vue:
|
||||||
|
title: "リスト"
|
||||||
create-list: "リストを作成"
|
create-list: "リストを作成"
|
||||||
|
|
||||||
desktop/views/components/user-preview.vue:
|
desktop/views/components/user-preview.vue:
|
||||||
|
@ -771,8 +780,10 @@ mobile/views/components/drive.file-detail.vue:
|
||||||
exif: "EXIF"
|
exif: "EXIF"
|
||||||
|
|
||||||
mobile/views/components/follow-button.vue:
|
mobile/views/components/follow-button.vue:
|
||||||
|
unfollow: "フォロー中"
|
||||||
follow: "フォロー"
|
follow: "フォロー"
|
||||||
unfollow: "フォロー解除"
|
request-pending: "フォロー許可待ち"
|
||||||
|
follow-request: "フォロー申請"
|
||||||
|
|
||||||
mobile/views/components/friends-maker.vue:
|
mobile/views/components/friends-maker.vue:
|
||||||
title: "気になるユーザーをフォロー"
|
title: "気になるユーザーをフォロー"
|
||||||
|
@ -841,6 +852,7 @@ mobile/views/components/ui.nav.vue:
|
||||||
timeline: "タイムライン"
|
timeline: "タイムライン"
|
||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
messaging: "メッセージ"
|
messaging: "メッセージ"
|
||||||
|
follow-requests: "フォロー申請"
|
||||||
search: "検索"
|
search: "検索"
|
||||||
drive: "ドライブ"
|
drive: "ドライブ"
|
||||||
favorites: "お気に入り"
|
favorites: "お気に入り"
|
||||||
|
@ -889,6 +901,11 @@ mobile/views/pages/messaging.vue:
|
||||||
mobile/views/pages/messaging-room.vue:
|
mobile/views/pages/messaging-room.vue:
|
||||||
messaging: "メッセージ"
|
messaging: "メッセージ"
|
||||||
|
|
||||||
|
mobile/views/pages/received-follow-requests.vue:
|
||||||
|
title: "フォロー申請"
|
||||||
|
accept: "承認"
|
||||||
|
reject: "拒否"
|
||||||
|
|
||||||
mobile/views/pages/note.vue:
|
mobile/views/pages/note.vue:
|
||||||
title: "投稿"
|
title: "投稿"
|
||||||
prev: "前の投稿"
|
prev: "前の投稿"
|
||||||
|
|
|
@ -20,7 +20,7 @@ export class HomeStream extends Stream {
|
||||||
}, 1000 * 60);
|
}, 1000 * 60);
|
||||||
|
|
||||||
// 自分の情報が更新されたとき
|
// 自分の情報が更新されたとき
|
||||||
this.on('i_updated', i => {
|
this.on('meUpdated', i => {
|
||||||
if (os.debug) {
|
if (os.debug) {
|
||||||
console.log('I updated:', i);
|
console.log('I updated:', i);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<button class="mk-follow-button"
|
<button class="mk-follow-button"
|
||||||
:class="{ wait, follow: !user.isFollowing, unfollow: user.isFollowing, big: size == 'big' }"
|
:class="{ wait, active: u.isFollowing || u.hasPendingFollowRequestFromYou, big: size == 'big' }"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
:disabled="wait"
|
:disabled="wait"
|
||||||
:title="user.isFollowing ? '%i18n:@unfollow%' : '%i18n:@follow%'"
|
|
||||||
>
|
>
|
||||||
<template v-if="!wait && user.isFollowing">
|
<template v-if="!wait">
|
||||||
<template v-if="size == 'compact'">%fa:minus%</template>
|
<template v-if="u.hasPendingFollowRequestFromYou">%fa:hourglass-half%<template v-if="size == 'big'"> %i18n:@request-pending%</template></template>
|
||||||
<template v-if="size == 'big'">%fa:minus%%i18n:@unfollow%</template>
|
<template v-else-if="u.isFollowing">%fa:minus%<template v-if="size == 'big'"> %i18n:@unfollow%</template></template>
|
||||||
|
<template v-else-if="!u.isFollowing && u.isLocked">%fa:plus%<template v-if="size == 'big'"> %i18n:@follow-request%</template></template>
|
||||||
|
<template v-else-if="!u.isFollowing && !u.isLocked">%fa:plus%<template v-if="size == 'big'"> %i18n:@follow%</template></template>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="!wait && !user.isFollowing">
|
<template v-else>%fa:spinner .pulse .fw%</template>
|
||||||
<template v-if="size == 'compact'">%fa:plus%</template>
|
|
||||||
<template v-if="size == 'big'">%fa:plus%%i18n:@follow%</template>
|
|
||||||
</template>
|
|
||||||
<template v-if="wait">%fa:spinner .pulse .fw%</template>
|
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -34,6 +31,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
u: this.user,
|
||||||
wait: false,
|
wait: false,
|
||||||
connection: null,
|
connection: null,
|
||||||
connectionId: null
|
connectionId: null
|
||||||
|
@ -56,39 +54,44 @@ export default Vue.extend({
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onFollow(user) {
|
onFollow(user) {
|
||||||
if (user.id == this.user.id) {
|
if (user.id == this.u.id) {
|
||||||
this.user.isFollowing = user.isFollowing;
|
this.user.isFollowing = user.isFollowing;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onUnfollow(user) {
|
onUnfollow(user) {
|
||||||
if (user.id == this.user.id) {
|
if (user.id == this.u.id) {
|
||||||
this.user.isFollowing = user.isFollowing;
|
this.user.isFollowing = user.isFollowing;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onClick() {
|
async onClick() {
|
||||||
this.wait = true;
|
this.wait = true;
|
||||||
if (this.user.isFollowing) {
|
|
||||||
(this as any).api('following/delete', {
|
try {
|
||||||
userId: this.user.id
|
if (this.u.isFollowing) {
|
||||||
}).then(() => {
|
this.u = await (this as any).api('following/delete', {
|
||||||
this.user.isFollowing = false;
|
userId: this.u.id
|
||||||
}).catch(err => {
|
});
|
||||||
console.error(err);
|
} else {
|
||||||
}).then(() => {
|
if (this.u.isLocked && this.u.hasPendingFollowRequestFromYou) {
|
||||||
this.wait = false;
|
this.u = await (this as any).api('following/requests/cancel', {
|
||||||
});
|
userId: this.u.id
|
||||||
} else {
|
});
|
||||||
(this as any).api('following/create', {
|
} else if (this.u.isLocked) {
|
||||||
userId: this.user.id
|
this.u = await (this as any).api('following/create', {
|
||||||
}).then(() => {
|
userId: this.u.id
|
||||||
this.user.isFollowing = true;
|
});
|
||||||
}).catch(err => {
|
} else {
|
||||||
console.error(err);
|
this.u = await (this as any).api('following/create', {
|
||||||
}).then(() => {
|
userId: this.user.id
|
||||||
this.wait = false;
|
});
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
this.wait = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,7 +127,7 @@ root(isDark)
|
||||||
border 2px solid rgba($theme-color, 0.3)
|
border 2px solid rgba($theme-color, 0.3)
|
||||||
border-radius 8px
|
border-radius 8px
|
||||||
|
|
||||||
&.follow
|
&:not(.active)
|
||||||
color isDark ? #fff : #888
|
color isDark ? #fff : #888
|
||||||
background isDark ? linear-gradient(to bottom, #313543 0%, #282c37 100%) : linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
|
background isDark ? linear-gradient(to bottom, #313543 0%, #282c37 100%) : linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
|
||||||
border solid 1px isDark ? #1c2023 : #e2e2e2
|
border solid 1px isDark ? #1c2023 : #e2e2e2
|
||||||
|
@ -137,7 +140,7 @@ root(isDark)
|
||||||
background isDark ? #22262f : #ececec
|
background isDark ? #22262f : #ececec
|
||||||
border-color isDark ? #151a1d : #dcdcdc
|
border-color isDark ? #151a1d : #dcdcdc
|
||||||
|
|
||||||
&.unfollow
|
&.active
|
||||||
color $theme-color-foreground
|
color $theme-color-foreground
|
||||||
background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
|
background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
|
||||||
border solid 1px lighten($theme-color, 15%)
|
border solid 1px lighten($theme-color, 15%)
|
||||||
|
@ -162,9 +165,6 @@ root(isDark)
|
||||||
height 38px
|
height 38px
|
||||||
line-height 38px
|
line-height 38px
|
||||||
|
|
||||||
i
|
|
||||||
margin-right 8px
|
|
||||||
|
|
||||||
.mk-follow-button[data-darkmode]
|
.mk-follow-button[data-darkmode]
|
||||||
root(true)
|
root(true)
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<template v-for="(notification, i) in _notifications">
|
<template v-for="(notification, i) in _notifications">
|
||||||
<div class="notification" :class="notification.type" :key="notification.id">
|
<div class="notification" :class="notification.type" :key="notification.id">
|
||||||
<mk-time :time="notification.createdAt"/>
|
<mk-time :time="notification.createdAt"/>
|
||||||
|
|
||||||
<template v-if="notification.type == 'reaction'">
|
<template v-if="notification.type == 'reaction'">
|
||||||
<mk-avatar class="avatar" :user="notification.user"/>
|
<mk-avatar class="avatar" :user="notification.user"/>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
|
@ -17,6 +18,7 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type == 'renote'">
|
<template v-if="notification.type == 'renote'">
|
||||||
<mk-avatar class="avatar" :user="notification.note.user"/>
|
<mk-avatar class="avatar" :user="notification.note.user"/>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
|
@ -28,6 +30,7 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type == 'quote'">
|
<template v-if="notification.type == 'quote'">
|
||||||
<mk-avatar class="avatar" :user="notification.note.user"/>
|
<mk-avatar class="avatar" :user="notification.note.user"/>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
|
@ -37,6 +40,7 @@
|
||||||
<router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
|
<router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type == 'follow'">
|
<template v-if="notification.type == 'follow'">
|
||||||
<mk-avatar class="avatar" :user="notification.user"/>
|
<mk-avatar class="avatar" :user="notification.user"/>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
|
@ -45,6 +49,16 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="notification.type == 'receiveFollowRequest'">
|
||||||
|
<mk-avatar class="avatar" :user="notification.user"/>
|
||||||
|
<div class="text">
|
||||||
|
<p>%fa:user-clock%
|
||||||
|
<router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type == 'reply'">
|
<template v-if="notification.type == 'reply'">
|
||||||
<mk-avatar class="avatar" :user="notification.note.user"/>
|
<mk-avatar class="avatar" :user="notification.note.user"/>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
|
@ -54,6 +68,7 @@
|
||||||
<router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
|
<router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type == 'mention'">
|
<template v-if="notification.type == 'mention'">
|
||||||
<mk-avatar class="avatar" :user="notification.note.user"/>
|
<mk-avatar class="avatar" :user="notification.note.user"/>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
|
@ -63,6 +78,7 @@
|
||||||
<a class="note-preview" :href="notification.note | notePage">{{ getNoteSummary(notification.note) }}</a>
|
<a class="note-preview" :href="notification.note | notePage">{{ getNoteSummary(notification.note) }}</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type == 'poll_vote'">
|
<template v-if="notification.type == 'poll_vote'">
|
||||||
<mk-avatar class="avatar" :user="notification.user"/>
|
<mk-avatar class="avatar" :user="notification.user"/>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
|
@ -73,6 +89,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
|
<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
|
||||||
<span>%fa:angle-up%{{ notification._datetext }}</span>
|
<span>%fa:angle-up%{{ notification._datetext }}</span>
|
||||||
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
|
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
|
||||||
|
@ -251,6 +268,10 @@ root(isDark)
|
||||||
.text p i
|
.text p i
|
||||||
color #53c7ce
|
color #53c7ce
|
||||||
|
|
||||||
|
&.receiveFollowRequest
|
||||||
|
.text p i
|
||||||
|
color #888
|
||||||
|
|
||||||
&.reply, &.mention
|
&.reply, &.mention
|
||||||
.text p i
|
.text p i
|
||||||
color #555
|
color #555
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
<template>
|
||||||
|
<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
|
||||||
|
<span slot="header">%fa:envelope R% %i18n:@title%</span>
|
||||||
|
|
||||||
|
<div data-id="c1136cec-1278-49b1-9ea7-412c1ef794f4" :data-darkmode="$store.state.device.darkmode">
|
||||||
|
<div v-for="req in requests">
|
||||||
|
<router-link :key="req.id" :to="req.follower | userPage">{{ req.follower | userName }}</router-link>
|
||||||
|
<span>
|
||||||
|
<a @click="accept(req.follower)">%i18n:@accept%</a>|<a @click="reject(req.follower)">%i18n:@reject%</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mk-window>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
export default Vue.extend({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
fetching: true,
|
||||||
|
requests: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
(this as any).api('following/requests/list').then(requests => {
|
||||||
|
this.fetching = false;
|
||||||
|
this.requests = requests;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
accept(user) {
|
||||||
|
(this as any).api('following/requests/accept', { userId: user.id }).then(() => {
|
||||||
|
this.requests = this.requests.filter(r => r.follower.id != user.id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
reject(user) {
|
||||||
|
(this as any).api('following/requests/reject', { userId: user.id }).then(() => {
|
||||||
|
this.requests = this.requests.filter(r => r.follower.id != user.id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
(this as any).$refs.window.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
|
||||||
|
root(isDark)
|
||||||
|
padding 16px
|
||||||
|
|
||||||
|
> button
|
||||||
|
margin-bottom 16px
|
||||||
|
|
||||||
|
> div
|
||||||
|
display flex
|
||||||
|
padding 16px
|
||||||
|
border solid 1px isDark ? #1c2023 : #eee
|
||||||
|
border-radius 4px
|
||||||
|
|
||||||
|
> span
|
||||||
|
margin 0 0 0 auto
|
||||||
|
|
||||||
|
[data-id="c1136cec-1278-49b1-9ea7-412c1ef794f4"][data-darkmode]
|
||||||
|
root(true)
|
||||||
|
|
||||||
|
[data-id="c1136cec-1278-49b1-9ea7-412c1ef794f4"]:not([data-darkmode])
|
||||||
|
root(false)
|
||||||
|
|
||||||
|
</style>
|
|
@ -23,7 +23,11 @@
|
||||||
</label>
|
</label>
|
||||||
<button class="ui primary" @click="save">%i18n:@save%</button>
|
<button class="ui primary" @click="save">%i18n:@save%</button>
|
||||||
<section>
|
<section>
|
||||||
<h2>その他</h2>
|
<h2>%i18n:@locked-account%</h2>
|
||||||
|
<mk-switch v-model="$store.state.i.isLocked" @change="onChangeIsLocked" text="%i18n:@is-locked%"/>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>%i18n:@other%</h2>
|
||||||
<mk-switch v-model="$store.state.i.isBot" @change="onChangeIsBot" text="%i18n:@is-bot%"/>
|
<mk-switch v-model="$store.state.i.isBot" @change="onChangeIsBot" text="%i18n:@is-bot%"/>
|
||||||
<mk-switch v-model="$store.state.i.isCat" @change="onChangeIsCat" text="%i18n:@is-cat%"/>
|
<mk-switch v-model="$store.state.i.isCat" @change="onChangeIsCat" text="%i18n:@is-cat%"/>
|
||||||
</section>
|
</section>
|
||||||
|
@ -62,6 +66,11 @@ export default Vue.extend({
|
||||||
(this as any).apis.notify('プロフィールを更新しました');
|
(this as any).apis.notify('プロフィールを更新しました');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onChangeIsLocked() {
|
||||||
|
(this as any).api('i/update', {
|
||||||
|
isLocked: this.$store.state.i.isLocked
|
||||||
|
});
|
||||||
|
},
|
||||||
onChangeIsBot() {
|
onChangeIsBot() {
|
||||||
(this as any).api('i/update', {
|
(this as any).api('i/update', {
|
||||||
isBot: this.$store.state.i.isBot
|
isBot: this.$store.state.i.isBot
|
||||||
|
|
|
@ -19,6 +19,9 @@
|
||||||
<li @click="list">
|
<li @click="list">
|
||||||
<p>%fa:list%<span>%i18n:@lists%</span>%fa:angle-right%</p>
|
<p>%fa:list%<span>%i18n:@lists%</span>%fa:angle-right%</p>
|
||||||
</li>
|
</li>
|
||||||
|
<li @click="followRequests" v-if="$store.state.i.isLocked">
|
||||||
|
<p>%fa:envelope R%<span>%i18n:@follow-requests%<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></span>%fa:angle-right%</p>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
|
@ -46,6 +49,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import MkUserListsWindow from './user-lists-window.vue';
|
import MkUserListsWindow from './user-lists-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 MkDriveWindow from './drive-window.vue';
|
||||||
import contains from '../../../common/scripts/contains';
|
import contains from '../../../common/scripts/contains';
|
||||||
|
@ -91,6 +95,10 @@ export default Vue.extend({
|
||||||
this.$router.push(`i/lists/${ list.id }`);
|
this.$router.push(`i/lists/${ list.id }`);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
followRequests() {
|
||||||
|
this.close();
|
||||||
|
(this as any).os.new(MkFollowRequestsWindow);
|
||||||
|
},
|
||||||
settings() {
|
settings() {
|
||||||
this.close();
|
this.close();
|
||||||
(this as any).os.new(MkSettingsWindow);
|
(this as any).os.new(MkSettingsWindow);
|
||||||
|
@ -225,6 +233,16 @@ root(isDark)
|
||||||
> span:first-child
|
> span:first-child
|
||||||
padding-left 22px
|
padding-left 22px
|
||||||
|
|
||||||
|
> span:nth-child(2)
|
||||||
|
> i
|
||||||
|
margin-left 4px
|
||||||
|
padding 2px 8px
|
||||||
|
font-size 90%
|
||||||
|
font-style normal
|
||||||
|
background $theme-color
|
||||||
|
color $theme-color-foreground
|
||||||
|
border-radius 8px
|
||||||
|
|
||||||
> [data-fa]:first-child
|
> [data-fa]:first-child
|
||||||
margin-right 6px
|
margin-right 6px
|
||||||
width 16px
|
width 16px
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
|
<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
|
||||||
<span slot="header">%fa:list% リスト</span>
|
<span slot="header">%fa:list% %i18n:@title%</span>
|
||||||
|
|
||||||
<div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="$store.state.device.darkmode">
|
<div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="$store.state.device.darkmode">
|
||||||
<button class="ui" @click="add">%i18n:@create-list%</button>
|
<button class="ui" @click="add">%i18n:@create-list%</button>
|
||||||
|
|
|
@ -32,6 +32,7 @@ import MkNotifications from './views/pages/notifications.vue';
|
||||||
import MkWidgets from './views/pages/widgets.vue';
|
import MkWidgets from './views/pages/widgets.vue';
|
||||||
import MkMessaging from './views/pages/messaging.vue';
|
import MkMessaging from './views/pages/messaging.vue';
|
||||||
import MkMessagingRoom from './views/pages/messaging-room.vue';
|
import MkMessagingRoom from './views/pages/messaging-room.vue';
|
||||||
|
import MkReceivedFollowRequests from './views/pages/received-follow-requests.vue';
|
||||||
import MkNote from './views/pages/note.vue';
|
import MkNote from './views/pages/note.vue';
|
||||||
import MkSearch from './views/pages/search.vue';
|
import MkSearch from './views/pages/search.vue';
|
||||||
import MkFollowers from './views/pages/followers.vue';
|
import MkFollowers from './views/pages/followers.vue';
|
||||||
|
@ -78,6 +79,7 @@ init((launch) => {
|
||||||
{ path: '/i/favorites', name: 'favorites', component: MkFavorites },
|
{ path: '/i/favorites', name: 'favorites', component: MkFavorites },
|
||||||
{ path: '/i/lists', name: 'user-lists', component: MkUserLists },
|
{ path: '/i/lists', name: 'user-lists', component: MkUserLists },
|
||||||
{ path: '/i/lists/:list', name: 'user-list', component: MkUserList },
|
{ path: '/i/lists/:list', name: 'user-list', component: MkUserList },
|
||||||
|
{ path: '/i/received-follow-requests', name: 'received-follow-requests', component: MkReceivedFollowRequests },
|
||||||
{ path: '/i/widgets', name: 'widgets', component: MkWidgets },
|
{ path: '/i/widgets', name: 'widgets', component: MkWidgets },
|
||||||
{ path: '/i/messaging', name: 'messaging', component: MkMessaging },
|
{ path: '/i/messaging', name: 'messaging', component: MkMessaging },
|
||||||
{ path: '/i/messaging/:user', component: MkMessagingRoom },
|
{ path: '/i/messaging/:user', component: MkMessagingRoom },
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<button class="mk-follow-button"
|
<button class="mk-follow-button"
|
||||||
:class="{ wait: wait, follow: !user.isFollowing, unfollow: user.isFollowing }"
|
:class="{ wait: wait, active: u.isFollowing || u.hasPendingFollowRequestFromYou }"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
:disabled="wait"
|
:disabled="wait"
|
||||||
>
|
>
|
||||||
<template v-if="!wait && user.isFollowing">%fa:minus%</template>
|
<template v-if="!wait">
|
||||||
<template v-if="!wait && !user.isFollowing">%fa:plus%</template>
|
<template v-if="u.hasPendingFollowRequestFromYou">%fa:hourglass-half% %i18n:@request-pending%</template>
|
||||||
<template v-if="wait">%fa:spinner .pulse .fw%</template>
|
<template v-else-if="u.isFollowing">%fa:minus% %i18n:@unfollow%</template>
|
||||||
{{ user.isFollowing ? '%i18n:@unfollow%' : '%i18n:@follow%' }}
|
<template v-else-if="!u.isFollowing && u.isLocked">%fa:plus% %i18n:@follow-request%</template>
|
||||||
|
<template v-else-if="!u.isFollowing && !u.isLocked">%fa:plus% %i18n:@follow%</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>%fa:spinner .pulse .fw%</template>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -22,6 +25,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
u: this.user,
|
||||||
wait: false,
|
wait: false,
|
||||||
connection: null,
|
connection: null,
|
||||||
connectionId: null
|
connectionId: null
|
||||||
|
@ -42,39 +46,44 @@ export default Vue.extend({
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
onFollow(user) {
|
onFollow(user) {
|
||||||
if (user.id == this.user.id) {
|
if (user.id == this.u.id) {
|
||||||
this.user.isFollowing = user.isFollowing;
|
this.u.isFollowing = user.isFollowing;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onUnfollow(user) {
|
onUnfollow(user) {
|
||||||
if (user.id == this.user.id) {
|
if (user.id == this.u.id) {
|
||||||
this.user.isFollowing = user.isFollowing;
|
this.u.isFollowing = user.isFollowing;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onClick() {
|
async onClick() {
|
||||||
this.wait = true;
|
this.wait = true;
|
||||||
if (this.user.isFollowing) {
|
|
||||||
(this as any).api('following/delete', {
|
try {
|
||||||
userId: this.user.id
|
if (this.u.isFollowing) {
|
||||||
}).then(() => {
|
this.u = await (this as any).api('following/delete', {
|
||||||
this.user.isFollowing = false;
|
userId: this.u.id
|
||||||
}).catch(err => {
|
});
|
||||||
console.error(err);
|
} else {
|
||||||
}).then(() => {
|
if (this.u.isLocked && this.u.hasPendingFollowRequestFromYou) {
|
||||||
this.wait = false;
|
this.u = await (this as any).api('following/requests/cancel', {
|
||||||
});
|
userId: this.u.id
|
||||||
} else {
|
});
|
||||||
(this as any).api('following/create', {
|
} else if (this.u.isLocked) {
|
||||||
userId: this.user.id
|
this.u = await (this as any).api('following/create', {
|
||||||
}).then(() => {
|
userId: this.u.id
|
||||||
this.user.isFollowing = true;
|
});
|
||||||
}).catch(err => {
|
} else {
|
||||||
console.error(err);
|
this.u = await (this as any).api('following/create', {
|
||||||
}).then(() => {
|
userId: this.user.id
|
||||||
this.wait = false;
|
});
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
this.wait = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,34 +99,38 @@ export default Vue.extend({
|
||||||
cursor pointer
|
cursor pointer
|
||||||
padding 0 16px
|
padding 0 16px
|
||||||
margin 0
|
margin 0
|
||||||
height inherit
|
min-width 150px
|
||||||
font-size 16px
|
line-height 36px
|
||||||
|
font-size 14px
|
||||||
|
color $theme-color
|
||||||
|
background transparent
|
||||||
outline none
|
outline none
|
||||||
border solid 1px $theme-color
|
border solid 1px $theme-color
|
||||||
border-radius 4px
|
border-radius 36px
|
||||||
|
|
||||||
*
|
&:hover
|
||||||
pointer-events none
|
background rgba($theme-color, 0.1)
|
||||||
|
|
||||||
&.follow
|
&:active
|
||||||
color $theme-color
|
background rgba($theme-color, 0.2)
|
||||||
background transparent
|
|
||||||
|
|
||||||
&:hover
|
&.active
|
||||||
background rgba($theme-color, 0.1)
|
|
||||||
|
|
||||||
&:active
|
|
||||||
background rgba($theme-color, 0.2)
|
|
||||||
|
|
||||||
&.unfollow
|
|
||||||
color $theme-color-foreground
|
color $theme-color-foreground
|
||||||
background $theme-color
|
background $theme-color
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background lighten($theme-color, 10%)
|
||||||
|
border-color lighten($theme-color, 10%)
|
||||||
|
|
||||||
|
&:active
|
||||||
|
background darken($theme-color, 10%)
|
||||||
|
border-color darken($theme-color, 10%)
|
||||||
|
|
||||||
&.wait
|
&.wait
|
||||||
cursor wait !important
|
cursor wait !important
|
||||||
opacity 0.7
|
opacity 0.7
|
||||||
|
|
||||||
> [data-fa]
|
*
|
||||||
margin-right 4px
|
pointer-events none
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mk-notification-preview" :class="notification.type">
|
<div class="mk-notification-preview" :class="notification.type">
|
||||||
<template v-if="notification.type == 'reaction'">
|
<template v-if="notification.type == 'reaction'">
|
||||||
<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
|
<mk-avatar class="avatar" :user="notification.user"/>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<p><mk-reaction-icon :reaction="notification.reaction"/>{{ notification.user | userName }}</p>
|
<p><mk-reaction-icon :reaction="notification.reaction"/>{{ notification.user | userName }}</p>
|
||||||
<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p>
|
<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p>
|
||||||
|
@ -9,7 +9,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type == 'renote'">
|
<template v-if="notification.type == 'renote'">
|
||||||
<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
|
<mk-avatar class="avatar" :user="notification.note.user"/>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<p>%fa:retweet%{{ notification.note.user | userName }}</p>
|
<p>%fa:retweet%{{ notification.note.user | userName }}</p>
|
||||||
<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%</p>
|
<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%</p>
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type == 'quote'">
|
<template v-if="notification.type == 'quote'">
|
||||||
<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
|
<mk-avatar class="avatar" :user="notification.note.user"/>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<p>%fa:quote-left%{{ notification.note.user | userName }}</p>
|
<p>%fa:quote-left%{{ notification.note.user | userName }}</p>
|
||||||
<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
|
<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
|
||||||
|
@ -25,14 +25,21 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type == 'follow'">
|
<template v-if="notification.type == 'follow'">
|
||||||
<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
|
<mk-avatar class="avatar" :user="notification.user"/>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<p>%fa:user-plus%{{ notification.user | userName }}</p>
|
<p>%fa:user-plus%{{ notification.user | userName }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="notification.type == 'receiveFollowRequest'">
|
||||||
|
<mk-avatar class="avatar" :user="notification.user"/>
|
||||||
|
<div class="text">
|
||||||
|
<p>%fa:user-clock%{{ notification.user | userName }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type == 'reply'">
|
<template v-if="notification.type == 'reply'">
|
||||||
<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
|
<mk-avatar class="avatar" :user="notification.note.user"/>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<p>%fa:reply%{{ notification.note.user | userName }}</p>
|
<p>%fa:reply%{{ notification.note.user | userName }}</p>
|
||||||
<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
|
<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
|
||||||
|
@ -40,7 +47,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type == 'mention'">
|
<template v-if="notification.type == 'mention'">
|
||||||
<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
|
<mk-avatar class="avatar" :user="notification.note.user"/>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<p>%fa:at%{{ notification.note.user | userName }}</p>
|
<p>%fa:at%{{ notification.note.user | userName }}</p>
|
||||||
<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
|
<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
|
||||||
|
@ -48,7 +55,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type == 'poll_vote'">
|
<template v-if="notification.type == 'poll_vote'">
|
||||||
<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
|
<mk-avatar class="avatar" :user="notification.user"/>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<p>%fa:chart-pie%{{ notification.user | userName }}</p>
|
<p>%fa:chart-pie%{{ notification.user | userName }}</p>
|
||||||
<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p>
|
<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p>
|
||||||
|
@ -83,16 +90,14 @@ export default Vue.extend({
|
||||||
display block
|
display block
|
||||||
clear both
|
clear both
|
||||||
|
|
||||||
img
|
> .avatar
|
||||||
display block
|
display block
|
||||||
float left
|
float left
|
||||||
min-width 36px
|
width 36px
|
||||||
min-height 36px
|
height 36px
|
||||||
max-width 36px
|
|
||||||
max-height 36px
|
|
||||||
border-radius 6px
|
border-radius 6px
|
||||||
|
|
||||||
.text
|
> .text
|
||||||
float right
|
float right
|
||||||
width calc(100% - 36px)
|
width calc(100% - 36px)
|
||||||
padding-left 8px
|
padding-left 8px
|
||||||
|
@ -120,6 +125,10 @@ export default Vue.extend({
|
||||||
.text p i
|
.text p i
|
||||||
color #53c7ce
|
color #53c7ce
|
||||||
|
|
||||||
|
&.receiveFollowRequest
|
||||||
|
.text p i
|
||||||
|
color #888
|
||||||
|
|
||||||
&.reply, &.mention
|
&.reply, &.mention
|
||||||
.text p i
|
.text p i
|
||||||
color #fff
|
color #fff
|
||||||
|
|
|
@ -40,6 +40,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="notification followRequest" v-if="notification.type == 'receiveFollowRequest'">
|
||||||
|
<mk-avatar class="avatar" :user="notification.user"/>
|
||||||
|
<div>
|
||||||
|
<header>
|
||||||
|
%fa:user-clock%
|
||||||
|
<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
|
||||||
|
<mk-time :time="notification.createdAt"/>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
|
<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
|
||||||
<mk-avatar class="avatar" :user="notification.user"/>
|
<mk-avatar class="avatar" :user="notification.user"/>
|
||||||
<div>
|
<div>
|
||||||
|
@ -156,6 +167,10 @@ root(isDark)
|
||||||
> div > header i
|
> div > header i
|
||||||
color #53c7ce
|
color #53c7ce
|
||||||
|
|
||||||
|
&.receiveFollowRequest
|
||||||
|
> div > header i
|
||||||
|
color #888
|
||||||
|
|
||||||
.mk-notification[data-darkmode]
|
.mk-notification[data-darkmode]
|
||||||
root(true)
|
root(true)
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
<li><router-link to="/" :data-active="$route.name == 'index'">%fa:home%%i18n:@timeline%%fa:angle-right%</router-link></li>
|
<li><router-link to="/" :data-active="$route.name == 'index'">%fa:home%%i18n:@timeline%%fa:angle-right%</router-link></li>
|
||||||
<li><router-link to="/i/notifications" :data-active="$route.name == 'notifications'">%fa:R bell%%i18n:@notifications%<template v-if="hasUnreadNotification">%fa:circle%</template>%fa:angle-right%</router-link></li>
|
<li><router-link to="/i/notifications" :data-active="$route.name == 'notifications'">%fa:R bell%%i18n:@notifications%<template v-if="hasUnreadNotification">%fa:circle%</template>%fa:angle-right%</router-link></li>
|
||||||
<li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'">%fa:R comments%%i18n:@messaging%<template v-if="hasUnreadMessagingMessage">%fa:circle%</template>%fa:angle-right%</router-link></li>
|
<li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'">%fa:R comments%%i18n:@messaging%<template v-if="hasUnreadMessagingMessage">%fa:circle%</template>%fa:angle-right%</router-link></li>
|
||||||
|
<li v-if="$store.getters.isSignedIn && $store.state.i.isLocked"><router-link to="/i/received-follow-requests" :data-active="$route.name == 'received-follow-requests'">%fa:R envelope%%i18n:@follow-requests%<template v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount">%fa:circle%</template>%fa:angle-right%</router-link></li>
|
||||||
<li><router-link to="/othello" :data-active="$route.name == 'othello'">%fa:gamepad%%i18n:@game%<template v-if="hasGameInvitation">%fa:circle%</template>%fa:angle-right%</router-link></li>
|
<li><router-link to="/othello" :data-active="$route.name == 'othello'">%fa:gamepad%%i18n:@game%<template v-if="hasGameInvitation">%fa:circle%</template>%fa:angle-right%</router-link></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<template>
|
||||||
|
<mk-ui>
|
||||||
|
<span slot="header">%fa:envelope R%%i18n:@title%</span>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div v-for="req in requests">
|
||||||
|
<router-link :key="req.id" :to="req.follower | userPage">{{ req.follower | userName }}</router-link>
|
||||||
|
<span>
|
||||||
|
<a @click="accept(req.follower)">%i18n:@accept%</a>|<a @click="reject(req.follower)">%i18n:@reject%</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</mk-ui>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Progress from '../../../common/scripts/loading';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
fetching: true,
|
||||||
|
requests: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
document.title = 'Misskey | %i18n:@title%';
|
||||||
|
|
||||||
|
Progress.start();
|
||||||
|
|
||||||
|
(this as any).api('following/requests/list').then(requests => {
|
||||||
|
this.fetching = false;
|
||||||
|
this.requests = requests;
|
||||||
|
|
||||||
|
Progress.done();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
accept(user) {
|
||||||
|
(this as any).api('following/requests/accept', { userId: user.id }).then(() => {
|
||||||
|
this.requests = this.requests.filter(r => r.follower.id != user.id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
reject(user) {
|
||||||
|
(this as any).api('following/requests/reject', { userId: user.id }).then(() => {
|
||||||
|
this.requests = this.requests.filter(r => r.follower.id != user.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
@import '~const.styl'
|
||||||
|
|
||||||
|
main
|
||||||
|
width 100%
|
||||||
|
max-width 680px
|
||||||
|
margin 0 auto
|
||||||
|
padding 8px
|
||||||
|
|
||||||
|
@media (min-width 500px)
|
||||||
|
padding 16px
|
||||||
|
|
||||||
|
@media (min-width 600px)
|
||||||
|
padding 32px
|
||||||
|
|
||||||
|
> div
|
||||||
|
display flex
|
||||||
|
padding 16px
|
||||||
|
border solid 1px isDark ? #1c2023 : #eee
|
||||||
|
border-radius 4px
|
||||||
|
|
||||||
|
> span
|
||||||
|
margin 0 0 0 auto
|
||||||
|
|
||||||
|
</style>
|
|
@ -184,7 +184,6 @@ root(isDark)
|
||||||
|
|
||||||
> .mk-follow-button
|
> .mk-follow-button
|
||||||
float right
|
float right
|
||||||
height 40px
|
|
||||||
|
|
||||||
> .title
|
> .title
|
||||||
margin 8px 0
|
margin 8px 0
|
||||||
|
|
87
src/models/follow-request.ts
Normal file
87
src/models/follow-request.ts
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import * as mongo from 'mongodb';
|
||||||
|
import * as deepcopy from 'deepcopy';
|
||||||
|
import db from '../db/mongodb';
|
||||||
|
import { pack as packUser } from './user';
|
||||||
|
|
||||||
|
const FollowRequest = db.get<IFollowRequest>('followRequests');
|
||||||
|
FollowRequest.createIndex(['followerId', 'followeeId'], { unique: true });
|
||||||
|
export default FollowRequest;
|
||||||
|
|
||||||
|
export type IFollowRequest = {
|
||||||
|
_id: mongo.ObjectID;
|
||||||
|
createdAt: Date;
|
||||||
|
followeeId: mongo.ObjectID;
|
||||||
|
followerId: mongo.ObjectID;
|
||||||
|
|
||||||
|
// 非正規化
|
||||||
|
_followee: {
|
||||||
|
host: string;
|
||||||
|
inbox?: string;
|
||||||
|
},
|
||||||
|
_follower: {
|
||||||
|
host: string;
|
||||||
|
inbox?: string;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FollowRequestを物理削除します
|
||||||
|
*/
|
||||||
|
export async function deleteFollowRequest(followRequest: string | mongo.ObjectID | IFollowRequest) {
|
||||||
|
let f: IFollowRequest;
|
||||||
|
|
||||||
|
// Populate
|
||||||
|
if (mongo.ObjectID.prototype.isPrototypeOf(followRequest)) {
|
||||||
|
f = await FollowRequest.findOne({
|
||||||
|
_id: followRequest
|
||||||
|
});
|
||||||
|
} else if (typeof followRequest === 'string') {
|
||||||
|
f = await FollowRequest.findOne({
|
||||||
|
_id: new mongo.ObjectID(followRequest)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
f = followRequest as IFollowRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f == null) return;
|
||||||
|
|
||||||
|
// このFollowingを削除
|
||||||
|
await FollowRequest.remove({
|
||||||
|
_id: f._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pack a request for API response
|
||||||
|
*/
|
||||||
|
export const pack = (
|
||||||
|
request: any,
|
||||||
|
me?: any
|
||||||
|
) => new Promise<any>(async (resolve, reject) => {
|
||||||
|
let _request: any;
|
||||||
|
|
||||||
|
// Populate the request if 'request' is ID
|
||||||
|
if (mongo.ObjectID.prototype.isPrototypeOf(request)) {
|
||||||
|
_request = await FollowRequest.findOne({
|
||||||
|
_id: request
|
||||||
|
});
|
||||||
|
} else if (typeof request === 'string') {
|
||||||
|
_request = await FollowRequest.findOne({
|
||||||
|
_id: new mongo.ObjectID(request)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_request = deepcopy(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename _id to id
|
||||||
|
_request.id = _request._id;
|
||||||
|
delete _request._id;
|
||||||
|
|
||||||
|
// Populate follower
|
||||||
|
_request.follower = await packUser(_request.followerId, me);
|
||||||
|
|
||||||
|
// Populate followee
|
||||||
|
_request.followee = await packUser(_request.followeeId, me);
|
||||||
|
|
||||||
|
resolve(_request);
|
||||||
|
});
|
|
@ -111,6 +111,7 @@ export const pack = (notification: any) => new Promise<any>(async (resolve, reje
|
||||||
|
|
||||||
switch (_notification.type) {
|
switch (_notification.type) {
|
||||||
case 'follow':
|
case 'follow':
|
||||||
|
case 'receiveFollowRequest':
|
||||||
// nope
|
// nope
|
||||||
break;
|
break;
|
||||||
case 'mention':
|
case 'mention':
|
||||||
|
|
|
@ -22,6 +22,7 @@ import FollowedLog, { deleteFollowedLog } from './followed-log';
|
||||||
import SwSubscription, { deleteSwSubscription } from './sw-subscription';
|
import SwSubscription, { deleteSwSubscription } from './sw-subscription';
|
||||||
import Notification, { deleteNotification } from './notification';
|
import Notification, { deleteNotification } from './notification';
|
||||||
import UserList, { deleteUserList } from './user-list';
|
import UserList, { deleteUserList } from './user-list';
|
||||||
|
import FollowRequest, { deleteFollowRequest } from './follow-request';
|
||||||
|
|
||||||
const User = db.get<IUser>('users');
|
const User = db.get<IUser>('users');
|
||||||
|
|
||||||
|
@ -50,7 +51,22 @@ type IUserBase = {
|
||||||
data: any;
|
data: any;
|
||||||
description: string;
|
description: string;
|
||||||
pinnedNoteId: mongo.ObjectID;
|
pinnedNoteId: mongo.ObjectID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 凍結されているか否か
|
||||||
|
*/
|
||||||
isSuspended: boolean;
|
isSuspended: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鍵アカウントか否か
|
||||||
|
*/
|
||||||
|
isLocked: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* このアカウントに届いているフォローリクエストの数
|
||||||
|
*/
|
||||||
|
pendingReceivedFollowRequestsCount: number;
|
||||||
|
|
||||||
host: string;
|
host: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -240,6 +256,16 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) {
|
||||||
await Following.find({ followeeId: u._id })
|
await Following.find({ followeeId: u._id })
|
||||||
).map(x => deleteFollowing(x)));
|
).map(x => deleteFollowing(x)));
|
||||||
|
|
||||||
|
// このユーザーのFollowRequestをすべて削除
|
||||||
|
await Promise.all((
|
||||||
|
await FollowRequest.find({ followerId: u._id })
|
||||||
|
).map(x => deleteFollowRequest(x)));
|
||||||
|
|
||||||
|
// このユーザーへのFollowRequestをすべて削除
|
||||||
|
await Promise.all((
|
||||||
|
await FollowRequest.find({ followeeId: u._id })
|
||||||
|
).map(x => deleteFollowRequest(x)));
|
||||||
|
|
||||||
// このユーザーのFollowingLogをすべて削除
|
// このユーザーのFollowingLogをすべて削除
|
||||||
await Promise.all((
|
await Promise.all((
|
||||||
await FollowingLog.find({ userId: u._id })
|
await FollowingLog.find({ userId: u._id })
|
||||||
|
@ -395,7 +421,7 @@ export const pack = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meId && !meId.equals(_user.id)) {
|
if (meId && !meId.equals(_user.id)) {
|
||||||
const [following1, following2, mute] = await Promise.all([
|
const [following1, following2, followReq1, followReq2, mute] = await Promise.all([
|
||||||
Following.findOne({
|
Following.findOne({
|
||||||
followerId: meId,
|
followerId: meId,
|
||||||
followeeId: _user.id
|
followeeId: _user.id
|
||||||
|
@ -404,6 +430,14 @@ export const pack = (
|
||||||
followerId: _user.id,
|
followerId: _user.id,
|
||||||
followeeId: meId
|
followeeId: meId
|
||||||
}),
|
}),
|
||||||
|
_user.isLocked ? FollowRequest.findOne({
|
||||||
|
followerId: meId,
|
||||||
|
followeeId: _user.id
|
||||||
|
}) : Promise.resolve(null),
|
||||||
|
FollowRequest.findOne({
|
||||||
|
followerId: _user.id,
|
||||||
|
followeeId: meId
|
||||||
|
}),
|
||||||
Mute.findOne({
|
Mute.findOne({
|
||||||
muterId: meId,
|
muterId: meId,
|
||||||
muteeId: _user.id
|
muteeId: _user.id
|
||||||
|
@ -414,6 +448,9 @@ export const pack = (
|
||||||
_user.isFollowing = following1 !== null;
|
_user.isFollowing = following1 !== null;
|
||||||
_user.isStalking = following1 && following1.stalk;
|
_user.isStalking = following1 && following1.stalk;
|
||||||
|
|
||||||
|
_user.hasPendingFollowRequestFromYou = followReq1 !== null;
|
||||||
|
_user.hasPendingFollowRequestToYou = followReq2 !== null;
|
||||||
|
|
||||||
// Whether the user is followed
|
// Whether the user is followed
|
||||||
_user.isFollowed = following2 !== null;
|
_user.isFollowed = following2 !== null;
|
||||||
|
|
||||||
|
|
27
src/remote/activitypub/kernel/accept/follow.ts
Normal file
27
src/remote/activitypub/kernel/accept/follow.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import * as mongo from 'mongodb';
|
||||||
|
import User, { IRemoteUser } from '../../../../models/user';
|
||||||
|
import config from '../../../../config';
|
||||||
|
import accept from '../../../../services/following/requests/accept';
|
||||||
|
import { IFollow } from '../../type';
|
||||||
|
|
||||||
|
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
|
||||||
|
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
|
||||||
|
|
||||||
|
if (!id.startsWith(config.url + '/')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const follower = await User.findOne({
|
||||||
|
_id: new mongo.ObjectID(id.split('/').pop())
|
||||||
|
});
|
||||||
|
|
||||||
|
if (follower === null) {
|
||||||
|
throw new Error('follower not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (follower.host != null) {
|
||||||
|
throw new Error('フォローリクエストしたユーザーはローカルユーザーではありません');
|
||||||
|
}
|
||||||
|
|
||||||
|
await accept(actor, follower);
|
||||||
|
};
|
35
src/remote/activitypub/kernel/accept/index.ts
Normal file
35
src/remote/activitypub/kernel/accept/index.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import * as debug from 'debug';
|
||||||
|
|
||||||
|
import Resolver from '../../resolver';
|
||||||
|
import { IRemoteUser } from '../../../../models/user';
|
||||||
|
import acceptFollow from './follow';
|
||||||
|
import { IAccept } from '../../type';
|
||||||
|
|
||||||
|
const log = debug('misskey:activitypub');
|
||||||
|
|
||||||
|
export default async (actor: IRemoteUser, activity: IAccept): Promise<void> => {
|
||||||
|
const uri = activity.id || activity;
|
||||||
|
|
||||||
|
log(`Accept: ${uri}`);
|
||||||
|
|
||||||
|
const resolver = new Resolver();
|
||||||
|
|
||||||
|
let object;
|
||||||
|
|
||||||
|
try {
|
||||||
|
object = await resolver.resolve(activity.object);
|
||||||
|
} catch (e) {
|
||||||
|
log(`Resolution failed: ${e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (object.type) {
|
||||||
|
case 'Follow':
|
||||||
|
acceptFollow(actor, object);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(`Unknown accept type: ${object.type}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
|
@ -23,5 +23,5 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
|
||||||
throw new Error('フォローしようとしているユーザーはローカルユーザーではありません');
|
throw new Error('フォローしようとしているユーザーはローカルユーザーではありません');
|
||||||
}
|
}
|
||||||
|
|
||||||
await follow(actor, followee, activity);
|
await follow(actor, followee);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,8 @@ import follow from './follow';
|
||||||
import undo from './undo';
|
import undo from './undo';
|
||||||
import like from './like';
|
import like from './like';
|
||||||
import announce from './announce';
|
import announce from './announce';
|
||||||
|
import accept from './accept';
|
||||||
|
import reject from './reject';
|
||||||
|
|
||||||
const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
|
const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
|
||||||
switch (activity.type) {
|
switch (activity.type) {
|
||||||
|
@ -22,7 +24,11 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Accept':
|
case 'Accept':
|
||||||
// noop
|
await accept(actor, activity);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Reject':
|
||||||
|
await reject(actor, activity);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Announce':
|
case 'Announce':
|
||||||
|
|
27
src/remote/activitypub/kernel/reject/follow.ts
Normal file
27
src/remote/activitypub/kernel/reject/follow.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import * as mongo from 'mongodb';
|
||||||
|
import User, { IRemoteUser } from '../../../../models/user';
|
||||||
|
import config from '../../../../config';
|
||||||
|
import reject from '../../../../services/following/requests/reject';
|
||||||
|
import { IFollow } from '../../type';
|
||||||
|
|
||||||
|
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
|
||||||
|
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
|
||||||
|
|
||||||
|
if (!id.startsWith(config.url + '/')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const follower = await User.findOne({
|
||||||
|
_id: new mongo.ObjectID(id.split('/').pop())
|
||||||
|
});
|
||||||
|
|
||||||
|
if (follower === null) {
|
||||||
|
throw new Error('follower not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (follower.host != null) {
|
||||||
|
throw new Error('フォローリクエストしたユーザーはローカルユーザーではありません');
|
||||||
|
}
|
||||||
|
|
||||||
|
await reject(actor, follower);
|
||||||
|
};
|
35
src/remote/activitypub/kernel/reject/index.ts
Normal file
35
src/remote/activitypub/kernel/reject/index.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import * as debug from 'debug';
|
||||||
|
|
||||||
|
import Resolver from '../../resolver';
|
||||||
|
import { IRemoteUser } from '../../../../models/user';
|
||||||
|
import rejectFollow from './follow';
|
||||||
|
import { IReject } from '../../type';
|
||||||
|
|
||||||
|
const log = debug('misskey:activitypub');
|
||||||
|
|
||||||
|
export default async (actor: IRemoteUser, activity: IReject): Promise<void> => {
|
||||||
|
const uri = activity.id || activity;
|
||||||
|
|
||||||
|
log(`Reject: ${uri}`);
|
||||||
|
|
||||||
|
const resolver = new Resolver();
|
||||||
|
|
||||||
|
let object;
|
||||||
|
|
||||||
|
try {
|
||||||
|
object = await resolver.resolve(activity.object);
|
||||||
|
} catch (e) {
|
||||||
|
log(`Resolution failed: ${e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (object.type) {
|
||||||
|
case 'Follow':
|
||||||
|
rejectFollow(actor, object);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(`Unknown reject type: ${object.type}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
|
@ -2,7 +2,9 @@ import * as mongo from 'mongodb';
|
||||||
import User, { IRemoteUser } from '../../../../models/user';
|
import User, { IRemoteUser } from '../../../../models/user';
|
||||||
import config from '../../../../config';
|
import config from '../../../../config';
|
||||||
import unfollow from '../../../../services/following/delete';
|
import unfollow from '../../../../services/following/delete';
|
||||||
|
import cancelRequest from '../../../../services/following/requests/cancel';
|
||||||
import { IFollow } from '../../type';
|
import { IFollow } from '../../type';
|
||||||
|
import FollowRequest from '../../../../models/follow-request';
|
||||||
|
|
||||||
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
|
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
|
||||||
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
|
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
|
||||||
|
@ -23,5 +25,14 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
|
||||||
throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません');
|
throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません');
|
||||||
}
|
}
|
||||||
|
|
||||||
await unfollow(actor, followee, activity);
|
const req = await FollowRequest.findOne({
|
||||||
|
followerId: actor._id,
|
||||||
|
followeeId: followee._id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (req) {
|
||||||
|
await cancelRequest(actor, followee);
|
||||||
|
} else {
|
||||||
|
await unfollow(actor, followee);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -93,6 +93,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
|
||||||
notesCount,
|
notesCount,
|
||||||
name: person.name,
|
name: person.name,
|
||||||
driveCapacity: 1024 * 1024 * 8, // 8MiB
|
driveCapacity: 1024 * 1024 * 8, // 8MiB
|
||||||
|
isLocked: person.manuallyApprovesFollowers,
|
||||||
username: person.preferredUsername,
|
username: person.preferredUsername,
|
||||||
usernameLower: person.preferredUsername.toLowerCase(),
|
usernameLower: person.preferredUsername.toLowerCase(),
|
||||||
host,
|
host,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
import { IRemoteUser, ILocalUser } from '../../../models/user';
|
import { IUser, isLocalUser } from '../../../models/user';
|
||||||
|
|
||||||
export default (follower: ILocalUser, followee: IRemoteUser) => ({
|
export default (follower: IUser, followee: IUser) => ({
|
||||||
type: 'Follow',
|
type: 'Follow',
|
||||||
actor: `${config.url}/users/${follower._id}`,
|
actor: isLocalUser(follower) ? `${config.url}/users/${follower._id}` : follower.uri,
|
||||||
object: followee.uri
|
object: isLocalUser(followee) ? `${config.url}/users/${followee._id}` : followee.uri
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import renderImage from './image';
|
import renderImage from './image';
|
||||||
import renderKey from './key';
|
import renderKey from './key';
|
||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
|
import { ILocalUser } from '../../../models/user';
|
||||||
|
|
||||||
export default user => {
|
export default (user: ILocalUser) => {
|
||||||
const id = `${config.url}/users/${user._id}`;
|
const id = `${config.url}/users/${user._id}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -17,6 +18,7 @@ export default user => {
|
||||||
summary: user.description,
|
summary: user.description,
|
||||||
icon: user.avatarId && renderImage({ _id: user.avatarId }),
|
icon: user.avatarId && renderImage({ _id: user.avatarId }),
|
||||||
image: user.bannerId && renderImage({ _id: user.bannerId }),
|
image: user.bannerId && renderImage({ _id: user.bannerId }),
|
||||||
|
manuallyApprovesFollowers: user.isLocked,
|
||||||
publicKey: renderKey(user)
|
publicKey: renderKey(user)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
4
src/remote/activitypub/renderer/reject.ts
Normal file
4
src/remote/activitypub/renderer/reject.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export default object => ({
|
||||||
|
type: 'Reject',
|
||||||
|
object
|
||||||
|
});
|
|
@ -45,6 +45,7 @@ export interface IPerson extends IObject {
|
||||||
type: 'Person';
|
type: 'Person';
|
||||||
name: string;
|
name: string;
|
||||||
preferredUsername: string;
|
preferredUsername: string;
|
||||||
|
manuallyApprovesFollowers: boolean;
|
||||||
inbox: string;
|
inbox: string;
|
||||||
publicKey: any;
|
publicKey: any;
|
||||||
followers: any;
|
followers: any;
|
||||||
|
@ -82,6 +83,10 @@ export interface IAccept extends IActivity {
|
||||||
type: 'Accept';
|
type: 'Accept';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IReject extends IActivity {
|
||||||
|
type: 'Reject';
|
||||||
|
}
|
||||||
|
|
||||||
export interface ILike extends IActivity {
|
export interface ILike extends IActivity {
|
||||||
type: 'Like';
|
type: 'Like';
|
||||||
_misskey_reaction: string;
|
_misskey_reaction: string;
|
||||||
|
@ -99,5 +104,6 @@ export type Object =
|
||||||
IUndo |
|
IUndo |
|
||||||
IFollow |
|
IFollow |
|
||||||
IAccept |
|
IAccept |
|
||||||
|
IReject |
|
||||||
ILike |
|
ILike |
|
||||||
IAnnounce;
|
IAnnounce;
|
||||||
|
|
|
@ -7,7 +7,7 @@ const httpSignature = require('http-signature');
|
||||||
import { createHttp } from '../queue';
|
import { createHttp } from '../queue';
|
||||||
import pack from '../remote/activitypub/renderer';
|
import pack from '../remote/activitypub/renderer';
|
||||||
import Note from '../models/note';
|
import Note from '../models/note';
|
||||||
import User, { isLocalUser } from '../models/user';
|
import User, { isLocalUser, ILocalUser } from '../models/user';
|
||||||
import renderNote from '../remote/activitypub/renderer/note';
|
import renderNote from '../remote/activitypub/renderer/note';
|
||||||
import renderKey from '../remote/activitypub/renderer/key';
|
import renderKey from '../remote/activitypub/renderer/key';
|
||||||
import renderPerson from '../remote/activitypub/renderer/person';
|
import renderPerson from '../remote/activitypub/renderer/person';
|
||||||
|
@ -69,7 +69,10 @@ router.get('/notes/:note', async (ctx, next) => {
|
||||||
router.get('/users/:user/outbox', async ctx => {
|
router.get('/users/:user/outbox', async ctx => {
|
||||||
const userId = new mongo.ObjectID(ctx.params.user);
|
const userId = new mongo.ObjectID(ctx.params.user);
|
||||||
|
|
||||||
const user = await User.findOne({ _id: userId });
|
const user = await User.findOne({
|
||||||
|
_id: userId,
|
||||||
|
host: null
|
||||||
|
});
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
|
@ -91,7 +94,10 @@ router.get('/users/:user/outbox', async ctx => {
|
||||||
router.get('/users/:user/publickey', async ctx => {
|
router.get('/users/:user/publickey', async ctx => {
|
||||||
const userId = new mongo.ObjectID(ctx.params.user);
|
const userId = new mongo.ObjectID(ctx.params.user);
|
||||||
|
|
||||||
const user = await User.findOne({ _id: userId });
|
const user = await User.findOne({
|
||||||
|
_id: userId,
|
||||||
|
host: null
|
||||||
|
});
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
|
@ -109,14 +115,17 @@ router.get('/users/:user/publickey', async ctx => {
|
||||||
router.get('/users/:user', async ctx => {
|
router.get('/users/:user', async ctx => {
|
||||||
const userId = new mongo.ObjectID(ctx.params.user);
|
const userId = new mongo.ObjectID(ctx.params.user);
|
||||||
|
|
||||||
const user = await User.findOne({ _id: userId });
|
const user = await User.findOne({
|
||||||
|
_id: userId,
|
||||||
|
host: null
|
||||||
|
});
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = pack(renderPerson(user));
|
ctx.body = pack(renderPerson(user as ILocalUser));
|
||||||
});
|
});
|
||||||
|
|
||||||
// follow form
|
// follow form
|
||||||
|
|
|
@ -448,6 +448,26 @@ const endpoints: Endpoint[] = [
|
||||||
},
|
},
|
||||||
kind: 'following-write'
|
kind: 'following-write'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'following/requests/accept',
|
||||||
|
withCredential: true,
|
||||||
|
kind: 'following-write'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'following/requests/reject',
|
||||||
|
withCredential: true,
|
||||||
|
kind: 'following-write'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'following/requests/cancel',
|
||||||
|
withCredential: true,
|
||||||
|
kind: 'following-write'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'following/requests/list',
|
||||||
|
withCredential: true,
|
||||||
|
kind: 'following-read'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'following/stalk',
|
name: 'following/stalk',
|
||||||
withCredential: true,
|
withCredential: true,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* Module dependencies
|
* Module dependencies
|
||||||
*/
|
*/
|
||||||
import $ from 'cafy'; import ID from '../../../../cafy-id';
|
import $ from 'cafy'; import ID from '../../../../cafy-id';
|
||||||
import User from '../../../../models/user';
|
import User, { pack } from '../../../../models/user';
|
||||||
import Following from '../../../../models/following';
|
import Following from '../../../../models/following';
|
||||||
import create from '../../../../services/following/create';
|
import create from '../../../../services/following/create';
|
||||||
|
|
||||||
|
@ -49,5 +49,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
create(follower, followee);
|
create(follower, followee);
|
||||||
|
|
||||||
// Send response
|
// Send response
|
||||||
res();
|
res(await pack(followee, user));
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* Module dependencies
|
* Module dependencies
|
||||||
*/
|
*/
|
||||||
import $ from 'cafy'; import ID from '../../../../cafy-id';
|
import $ from 'cafy'; import ID from '../../../../cafy-id';
|
||||||
import User from '../../../../models/user';
|
import User, { pack } from '../../../../models/user';
|
||||||
import Following from '../../../../models/following';
|
import Following from '../../../../models/following';
|
||||||
import deleteFollowing from '../../../../services/following/delete';
|
import deleteFollowing from '../../../../services/following/delete';
|
||||||
|
|
||||||
|
@ -49,5 +49,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
deleteFollowing(follower, followee);
|
deleteFollowing(follower, followee);
|
||||||
|
|
||||||
// Send response
|
// Send response
|
||||||
res();
|
res(await pack(followee, user));
|
||||||
});
|
});
|
||||||
|
|
26
src/server/api/endpoints/following/requests/accept.ts
Normal file
26
src/server/api/endpoints/following/requests/accept.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import $ from 'cafy'; import ID from '../../../../../cafy-id';
|
||||||
|
import acceptFollowRequest from '../../../../../services/following/requests/accept';
|
||||||
|
import User from '../../../../../models/user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept a follow request
|
||||||
|
*/
|
||||||
|
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
|
// Get 'userId' parameter
|
||||||
|
const [followerId, followerIdErr] = $.type(ID).get(params.userId);
|
||||||
|
if (followerIdErr) return rej('invalid userId param');
|
||||||
|
|
||||||
|
// Fetch follower
|
||||||
|
const follower = await User.findOne({
|
||||||
|
_id: followerId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (follower === null) {
|
||||||
|
return rej('follower not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await acceptFollowRequest(user, follower);
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
res();
|
||||||
|
});
|
26
src/server/api/endpoints/following/requests/cancel.ts
Normal file
26
src/server/api/endpoints/following/requests/cancel.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import $ from 'cafy'; import ID from '../../../../../cafy-id';
|
||||||
|
import cancelFollowRequest from '../../../../../services/following/requests/cancel';
|
||||||
|
import User, { pack } from '../../../../../models/user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a follow request
|
||||||
|
*/
|
||||||
|
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
|
// Get 'userId' parameter
|
||||||
|
const [followeeId, followeeIdErr] = $.type(ID).get(params.userId);
|
||||||
|
if (followeeIdErr) return rej('invalid userId param');
|
||||||
|
|
||||||
|
// Fetch followee
|
||||||
|
const followee = await User.findOne({
|
||||||
|
_id: followeeId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (followee === null) {
|
||||||
|
return rej('followee not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await cancelFollowRequest(followee, user);
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
res(await pack(followee._id, user));
|
||||||
|
});
|
14
src/server/api/endpoints/following/requests/list.ts
Normal file
14
src/server/api/endpoints/following/requests/list.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
//import $ from 'cafy'; import ID from '../../../../../cafy-id';
|
||||||
|
import FollowRequest, { pack } from '../../../../../models/follow-request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all pending received follow requests
|
||||||
|
*/
|
||||||
|
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
|
const reqs = await FollowRequest.find({
|
||||||
|
followeeId: user._id
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
res(await Promise.all(reqs.map(req => pack(req))));
|
||||||
|
});
|
26
src/server/api/endpoints/following/requests/reject.ts
Normal file
26
src/server/api/endpoints/following/requests/reject.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import $ from 'cafy'; import ID from '../../../../../cafy-id';
|
||||||
|
import rejectFollowRequest from '../../../../../services/following/requests/reject';
|
||||||
|
import User from '../../../../../models/user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject a follow request
|
||||||
|
*/
|
||||||
|
module.exports = (params, user) => new Promise(async (res, rej) => {
|
||||||
|
// Get 'userId' parameter
|
||||||
|
const [followerId, followerIdErr] = $.type(ID).get(params.userId);
|
||||||
|
if (followerIdErr) return rej('invalid userId param');
|
||||||
|
|
||||||
|
// Fetch follower
|
||||||
|
const follower = await User.findOne({
|
||||||
|
_id: followerId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (follower === null) {
|
||||||
|
return rej('follower not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await rejectFollowRequest(user, follower);
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
res();
|
||||||
|
});
|
|
@ -5,6 +5,7 @@ import $ from 'cafy'; import ID from '../../../../cafy-id';
|
||||||
import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user';
|
import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user';
|
||||||
import event from '../../../../publishers/stream';
|
import event from '../../../../publishers/stream';
|
||||||
import DriveFile from '../../../../models/drive-file';
|
import DriveFile from '../../../../models/drive-file';
|
||||||
|
import acceptAllFollowRequests from '../../../../services/following/requests/accept-all';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update myself
|
* Update myself
|
||||||
|
@ -12,50 +13,57 @@ import DriveFile from '../../../../models/drive-file';
|
||||||
module.exports = async (params, user, app) => new Promise(async (res, rej) => {
|
module.exports = async (params, user, app) => new Promise(async (res, rej) => {
|
||||||
const isSecure = user != null && app == null;
|
const isSecure = user != null && app == null;
|
||||||
|
|
||||||
|
const updates = {} as any;
|
||||||
|
|
||||||
// Get 'name' parameter
|
// Get 'name' parameter
|
||||||
const [name, nameErr] = $.str.optional().nullable().pipe(isValidName).get(params.name);
|
const [name, nameErr] = $.str.optional().nullable().pipe(isValidName).get(params.name);
|
||||||
if (nameErr) return rej('invalid name param');
|
if (nameErr) return rej('invalid name param');
|
||||||
if (name) user.name = name;
|
if (name) updates.name = name;
|
||||||
|
|
||||||
// Get 'description' parameter
|
// Get 'description' parameter
|
||||||
const [description, descriptionErr] = $.str.optional().nullable().pipe(isValidDescription).get(params.description);
|
const [description, descriptionErr] = $.str.optional().nullable().pipe(isValidDescription).get(params.description);
|
||||||
if (descriptionErr) return rej('invalid description param');
|
if (descriptionErr) return rej('invalid description param');
|
||||||
if (description !== undefined) user.description = description;
|
if (description !== undefined) updates.description = description;
|
||||||
|
|
||||||
// Get 'location' parameter
|
// Get 'location' parameter
|
||||||
const [location, locationErr] = $.str.optional().nullable().pipe(isValidLocation).get(params.location);
|
const [location, locationErr] = $.str.optional().nullable().pipe(isValidLocation).get(params.location);
|
||||||
if (locationErr) return rej('invalid location param');
|
if (locationErr) return rej('invalid location param');
|
||||||
if (location !== undefined) user.profile.location = location;
|
if (location !== undefined) updates['profile.location'] = location;
|
||||||
|
|
||||||
// Get 'birthday' parameter
|
// Get 'birthday' parameter
|
||||||
const [birthday, birthdayErr] = $.str.optional().nullable().pipe(isValidBirthday).get(params.birthday);
|
const [birthday, birthdayErr] = $.str.optional().nullable().pipe(isValidBirthday).get(params.birthday);
|
||||||
if (birthdayErr) return rej('invalid birthday param');
|
if (birthdayErr) return rej('invalid birthday param');
|
||||||
if (birthday !== undefined) user.profile.birthday = birthday;
|
if (birthday !== undefined) updates['profile.birthday'] = birthday;
|
||||||
|
|
||||||
// Get 'avatarId' parameter
|
// Get 'avatarId' parameter
|
||||||
const [avatarId, avatarIdErr] = $.type(ID).optional().get(params.avatarId);
|
const [avatarId, avatarIdErr] = $.type(ID).optional().nullable().get(params.avatarId);
|
||||||
if (avatarIdErr) return rej('invalid avatarId param');
|
if (avatarIdErr) return rej('invalid avatarId param');
|
||||||
if (avatarId) user.avatarId = avatarId;
|
if (avatarId !== undefined) updates.avatarId = avatarId;
|
||||||
|
|
||||||
// Get 'bannerId' parameter
|
// Get 'bannerId' parameter
|
||||||
const [bannerId, bannerIdErr] = $.type(ID).optional().get(params.bannerId);
|
const [bannerId, bannerIdErr] = $.type(ID).optional().nullable().get(params.bannerId);
|
||||||
if (bannerIdErr) return rej('invalid bannerId param');
|
if (bannerIdErr) return rej('invalid bannerId param');
|
||||||
if (bannerId) user.bannerId = bannerId;
|
if (bannerId !== undefined) updates.bannerId = bannerId;
|
||||||
|
|
||||||
|
// Get 'isLocked' parameter
|
||||||
|
const [isLocked, isLockedErr] = $.bool.optional().get(params.isLocked);
|
||||||
|
if (isLockedErr) return rej('invalid isLocked param');
|
||||||
|
if (isLocked != null) updates.isLocked = isLocked;
|
||||||
|
|
||||||
// Get 'isBot' parameter
|
// Get 'isBot' parameter
|
||||||
const [isBot, isBotErr] = $.bool.optional().get(params.isBot);
|
const [isBot, isBotErr] = $.bool.optional().get(params.isBot);
|
||||||
if (isBotErr) return rej('invalid isBot param');
|
if (isBotErr) return rej('invalid isBot param');
|
||||||
if (isBot != null) user.isBot = isBot;
|
if (isBot != null) updates.isBot = isBot;
|
||||||
|
|
||||||
// Get 'isCat' parameter
|
// Get 'isCat' parameter
|
||||||
const [isCat, isCatErr] = $.bool.optional().get(params.isCat);
|
const [isCat, isCatErr] = $.bool.optional().get(params.isCat);
|
||||||
if (isCatErr) return rej('invalid isCat param');
|
if (isCatErr) return rej('invalid isCat param');
|
||||||
if (isCat != null) user.isCat = isCat;
|
if (isCat != null) updates.isCat = isCat;
|
||||||
|
|
||||||
// Get 'autoWatch' parameter
|
// Get 'autoWatch' parameter
|
||||||
const [autoWatch, autoWatchErr] = $.bool.optional().get(params.autoWatch);
|
const [autoWatch, autoWatchErr] = $.bool.optional().get(params.autoWatch);
|
||||||
if (autoWatchErr) return rej('invalid autoWatch param');
|
if (autoWatchErr) return rej('invalid autoWatch param');
|
||||||
if (autoWatch != null) user.settings.autoWatch = autoWatch;
|
if (autoWatch != null) updates['settings.autoWatch'] = autoWatch;
|
||||||
|
|
||||||
if (avatarId) {
|
if (avatarId) {
|
||||||
const avatar = await DriveFile.findOne({
|
const avatar = await DriveFile.findOne({
|
||||||
|
@ -63,7 +71,7 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (avatar != null && avatar.metadata.properties.avgColor) {
|
if (avatar != null && avatar.metadata.properties.avgColor) {
|
||||||
user.avatarColor = avatar.metadata.properties.avgColor;
|
updates.avatarColor = avatar.metadata.properties.avgColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,27 +81,16 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (banner != null && banner.metadata.properties.avgColor) {
|
if (banner != null && banner.metadata.properties.avgColor) {
|
||||||
user.bannerColor = banner.metadata.properties.avgColor;
|
updates.bannerColor = banner.metadata.properties.avgColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await User.update(user._id, {
|
await User.update(user._id, {
|
||||||
$set: {
|
$set: updates
|
||||||
name: user.name,
|
|
||||||
description: user.description,
|
|
||||||
avatarId: user.avatarId,
|
|
||||||
avatarColor: user.avatarColor,
|
|
||||||
bannerId: user.bannerId,
|
|
||||||
bannerColor: user.bannerColor,
|
|
||||||
profile: user.profile,
|
|
||||||
isBot: user.isBot,
|
|
||||||
isCat: user.isCat,
|
|
||||||
settings: user.settings
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Serialize
|
// Serialize
|
||||||
const iObj = await pack(user, user, {
|
const iObj = await pack(user._id, user, {
|
||||||
detail: true,
|
detail: true,
|
||||||
includeSecrets: isSecure
|
includeSecrets: isSecure
|
||||||
});
|
});
|
||||||
|
@ -101,6 +98,11 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => {
|
||||||
// Send response
|
// Send response
|
||||||
res(iObj);
|
res(iObj);
|
||||||
|
|
||||||
// Publish i updated event
|
// Publish meUpdated event
|
||||||
event(user._id, 'i_updated', iObj);
|
event(user._id, 'meUpdated', iObj);
|
||||||
|
|
||||||
|
// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
|
||||||
|
if (user.isLocked && isLocked === false) {
|
||||||
|
acceptAllFollowRequests(user);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -36,6 +36,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
|
||||||
_id: {
|
_id: {
|
||||||
$nin: followingIds.concat(mutedUserIds)
|
$nin: followingIds.concat(mutedUserIds)
|
||||||
},
|
},
|
||||||
|
isLocked: false,
|
||||||
$or: [{
|
$or: [{
|
||||||
lastUsedAt: {
|
lastUsedAt: {
|
||||||
$gte: new Date(Date.now() - ms('7days'))
|
$gte: new Date(Date.now() - ms('7days'))
|
||||||
|
|
|
@ -49,7 +49,7 @@ router.get('/disconnect/twitter', async ctx => {
|
||||||
ctx.body = `Twitterの連携を解除しました :v:`;
|
ctx.body = `Twitterの連携を解除しました :v:`;
|
||||||
|
|
||||||
// Publish i updated event
|
// Publish i updated event
|
||||||
event(user._id, 'i_updated', await pack(user, user, {
|
event(user._id, 'meUpdated', await pack(user, user, {
|
||||||
detail: true,
|
detail: true,
|
||||||
includeSecrets: true
|
includeSecrets: true
|
||||||
}));
|
}));
|
||||||
|
@ -174,7 +174,7 @@ if (config.twitter == null) {
|
||||||
ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
|
ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
|
||||||
|
|
||||||
// Publish i updated event
|
// Publish i updated event
|
||||||
event(user._id, 'i_updated', await pack(user, user, {
|
event(user._id, 'meUpdated', await pack(user, user, {
|
||||||
detail: true,
|
detail: true,
|
||||||
includeSecrets: true
|
includeSecrets: true
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -8,72 +8,76 @@ import pack from '../../remote/activitypub/renderer';
|
||||||
import renderFollow from '../../remote/activitypub/renderer/follow';
|
import renderFollow from '../../remote/activitypub/renderer/follow';
|
||||||
import renderAccept from '../../remote/activitypub/renderer/accept';
|
import renderAccept from '../../remote/activitypub/renderer/accept';
|
||||||
import { deliver } from '../../queue';
|
import { deliver } from '../../queue';
|
||||||
|
import createFollowRequest from './requests/create';
|
||||||
|
|
||||||
export default async function(follower: IUser, followee: IUser, activity?) {
|
export default async function(follower: IUser, followee: IUser) {
|
||||||
const following = await Following.insert({
|
if (followee.isLocked) {
|
||||||
createdAt: new Date(),
|
await createFollowRequest(follower, followee);
|
||||||
followerId: follower._id,
|
} else {
|
||||||
followeeId: followee._id,
|
const following = await Following.insert({
|
||||||
stalk: true,
|
createdAt: new Date(),
|
||||||
|
followerId: follower._id,
|
||||||
|
followeeId: followee._id,
|
||||||
|
|
||||||
// 非正規化
|
// 非正規化
|
||||||
_follower: {
|
_follower: {
|
||||||
host: follower.host,
|
host: follower.host,
|
||||||
inbox: isRemoteUser(follower) ? follower.inbox : undefined
|
inbox: isRemoteUser(follower) ? follower.inbox : undefined
|
||||||
},
|
},
|
||||||
_followee: {
|
_followee: {
|
||||||
host: followee.host,
|
host: followee.host,
|
||||||
inbox: isRemoteUser(followee) ? followee.inbox : undefined
|
inbox: isRemoteUser(followee) ? followee.inbox : undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//#region Increment following count
|
||||||
|
User.update({ _id: follower._id }, {
|
||||||
|
$inc: {
|
||||||
|
followingCount: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
FollowingLog.insert({
|
||||||
|
createdAt: following.createdAt,
|
||||||
|
userId: follower._id,
|
||||||
|
count: follower.followingCount + 1
|
||||||
|
});
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Increment followers count
|
||||||
|
User.update({ _id: followee._id }, {
|
||||||
|
$inc: {
|
||||||
|
followersCount: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
FollowedLog.insert({
|
||||||
|
createdAt: following.createdAt,
|
||||||
|
userId: followee._id,
|
||||||
|
count: followee.followersCount + 1
|
||||||
|
});
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
// Publish follow event
|
||||||
|
if (isLocalUser(follower)) {
|
||||||
|
packUser(followee, follower).then(packed => event(follower._id, 'follow', packed));
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
//#region Increment following count
|
// Publish followed event
|
||||||
User.update({ _id: follower._id }, {
|
if (isLocalUser(followee)) {
|
||||||
$inc: {
|
packUser(follower, followee).then(packed => event(followee._id, 'followed', packed)),
|
||||||
followingCount: 1
|
|
||||||
|
// 通知を作成
|
||||||
|
notify(followee._id, follower._id, 'follow');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
FollowingLog.insert({
|
if (isLocalUser(follower) && isRemoteUser(followee)) {
|
||||||
createdAt: following.createdAt,
|
const content = pack(renderFollow(follower, followee));
|
||||||
userId: follower._id,
|
deliver(follower, content, followee.inbox);
|
||||||
count: follower.followingCount + 1
|
|
||||||
});
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region Increment followers count
|
|
||||||
User.update({ _id: followee._id }, {
|
|
||||||
$inc: {
|
|
||||||
followersCount: 1
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
FollowedLog.insert({
|
|
||||||
createdAt: following.createdAt,
|
|
||||||
userId: followee._id,
|
|
||||||
count: followee.followersCount + 1
|
|
||||||
});
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
// Publish follow event
|
if (isRemoteUser(follower) && isLocalUser(followee)) {
|
||||||
if (isLocalUser(follower)) {
|
const content = pack(renderAccept(renderFollow(follower, followee)));
|
||||||
packUser(followee, follower).then(packed => event(follower._id, 'follow', packed));
|
deliver(followee, content, follower.inbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish followed event
|
|
||||||
if (isLocalUser(followee)) {
|
|
||||||
packUser(follower, followee).then(packed => event(followee._id, 'followed', packed)),
|
|
||||||
|
|
||||||
// 通知を作成
|
|
||||||
notify(followee._id, follower._id, 'follow');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLocalUser(follower) && isRemoteUser(followee)) {
|
|
||||||
const content = pack(renderFollow(follower, followee));
|
|
||||||
deliver(follower, content, followee.inbox);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRemoteUser(follower) && isLocalUser(followee)) {
|
|
||||||
const content = pack(renderAccept(activity));
|
|
||||||
deliver(followee, content, follower.inbox);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import renderFollow from '../../remote/activitypub/renderer/follow';
|
||||||
import renderUndo from '../../remote/activitypub/renderer/undo';
|
import renderUndo from '../../remote/activitypub/renderer/undo';
|
||||||
import { deliver } from '../../queue';
|
import { deliver } from '../../queue';
|
||||||
|
|
||||||
export default async function(follower: IUser, followee: IUser, activity?) {
|
export default async function(follower: IUser, followee: IUser) {
|
||||||
const following = await Following.findOne({
|
const following = await Following.findOne({
|
||||||
followerId: follower._id,
|
followerId: follower._id,
|
||||||
followeeId: followee._id
|
followeeId: followee._id
|
||||||
|
|
24
src/services/following/requests/accept-all.ts
Normal file
24
src/services/following/requests/accept-all.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import User, { IUser } from "../../../models/user";
|
||||||
|
import FollowRequest from "../../../models/follow-request";
|
||||||
|
import accept from './accept';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指定したユーザー宛てのフォローリクエストをすべて承認
|
||||||
|
* @param user ユーザー
|
||||||
|
*/
|
||||||
|
export default async function(user: IUser) {
|
||||||
|
const requests = await FollowRequest.find({
|
||||||
|
followeeId: user._id
|
||||||
|
});
|
||||||
|
|
||||||
|
requests.forEach(async request => {
|
||||||
|
const follower = await User.findOne({ _id: request.followerId });
|
||||||
|
accept(user, follower);
|
||||||
|
});
|
||||||
|
|
||||||
|
User.update({ _id: user._id }, {
|
||||||
|
$set: {
|
||||||
|
pendingReceivedFollowRequestsCount: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
70
src/services/following/requests/accept.ts
Normal file
70
src/services/following/requests/accept.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from "../../../models/user";
|
||||||
|
import FollowRequest from "../../../models/follow-request";
|
||||||
|
import pack from '../../../remote/activitypub/renderer';
|
||||||
|
import renderFollow from '../../../remote/activitypub/renderer/follow';
|
||||||
|
import renderAccept from '../../../remote/activitypub/renderer/accept';
|
||||||
|
import { deliver } from '../../../queue';
|
||||||
|
import Following from "../../../models/following";
|
||||||
|
import FollowingLog from "../../../models/following-log";
|
||||||
|
import FollowedLog from "../../../models/followed-log";
|
||||||
|
import event from '../../../publishers/stream';
|
||||||
|
|
||||||
|
export default async function(followee: IUser, follower: IUser) {
|
||||||
|
const following = await Following.insert({
|
||||||
|
createdAt: new Date(),
|
||||||
|
followerId: follower._id,
|
||||||
|
followeeId: followee._id,
|
||||||
|
|
||||||
|
// 非正規化
|
||||||
|
_follower: {
|
||||||
|
host: follower.host,
|
||||||
|
inbox: isRemoteUser(follower) ? follower.inbox : undefined
|
||||||
|
},
|
||||||
|
_followee: {
|
||||||
|
host: followee.host,
|
||||||
|
inbox: isRemoteUser(followee) ? followee.inbox : undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isRemoteUser(follower)) {
|
||||||
|
const content = pack(renderAccept(renderFollow(follower, followee)));
|
||||||
|
deliver(followee as ILocalUser, content, follower.inbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
await FollowRequest.remove({
|
||||||
|
followeeId: followee._id,
|
||||||
|
followerId: follower._id
|
||||||
|
});
|
||||||
|
|
||||||
|
//#region Increment following count
|
||||||
|
await User.update({ _id: follower._id }, {
|
||||||
|
$inc: {
|
||||||
|
followingCount: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
FollowingLog.insert({
|
||||||
|
createdAt: following.createdAt,
|
||||||
|
userId: follower._id,
|
||||||
|
count: follower.followingCount + 1
|
||||||
|
});
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Increment followers count
|
||||||
|
await User.update({ _id: followee._id }, {
|
||||||
|
$inc: {
|
||||||
|
followersCount: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
FollowedLog.insert({
|
||||||
|
createdAt: following.createdAt,
|
||||||
|
userId: followee._id,
|
||||||
|
count: followee.followersCount + 1
|
||||||
|
});
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
packUser(followee, followee, {
|
||||||
|
detail: true
|
||||||
|
}).then(packed => event(followee._id, 'meUpdated', packed));
|
||||||
|
}
|
29
src/services/following/requests/cancel.ts
Normal file
29
src/services/following/requests/cancel.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from "../../../models/user";
|
||||||
|
import FollowRequest from "../../../models/follow-request";
|
||||||
|
import pack from '../../../remote/activitypub/renderer';
|
||||||
|
import renderFollow from '../../../remote/activitypub/renderer/follow';
|
||||||
|
import renderUndo from '../../../remote/activitypub/renderer/undo';
|
||||||
|
import { deliver } from '../../../queue';
|
||||||
|
import event from '../../../publishers/stream';
|
||||||
|
|
||||||
|
export default async function(followee: IUser, follower: IUser) {
|
||||||
|
if (isRemoteUser(followee)) {
|
||||||
|
const content = pack(renderUndo(renderFollow(follower, followee)));
|
||||||
|
deliver(follower as ILocalUser, content, followee.inbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
await FollowRequest.remove({
|
||||||
|
followeeId: followee._id,
|
||||||
|
followerId: follower._id
|
||||||
|
});
|
||||||
|
|
||||||
|
await User.update({ _id: followee._id }, {
|
||||||
|
$inc: {
|
||||||
|
pendingReceivedFollowRequestsCount: -1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
packUser(followee, followee, {
|
||||||
|
detail: true
|
||||||
|
}).then(packed => event(followee._id, 'meUpdated', packed));
|
||||||
|
}
|
50
src/services/following/requests/create.ts
Normal file
50
src/services/following/requests/create.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../../models/user';
|
||||||
|
import event from '../../../publishers/stream';
|
||||||
|
import notify from '../../../publishers/notify';
|
||||||
|
import pack from '../../../remote/activitypub/renderer';
|
||||||
|
import renderFollow from '../../../remote/activitypub/renderer/follow';
|
||||||
|
import { deliver } from '../../../queue';
|
||||||
|
import FollowRequest from '../../../models/follow-request';
|
||||||
|
|
||||||
|
export default async function(follower: IUser, followee: IUser) {
|
||||||
|
if (!followee.isLocked) throw '対象のアカウントは鍵アカウントではありません';
|
||||||
|
|
||||||
|
await FollowRequest.insert({
|
||||||
|
createdAt: new Date(),
|
||||||
|
followerId: follower._id,
|
||||||
|
followeeId: followee._id,
|
||||||
|
|
||||||
|
// 非正規化
|
||||||
|
_follower: {
|
||||||
|
host: follower.host,
|
||||||
|
inbox: isRemoteUser(follower) ? follower.inbox : undefined
|
||||||
|
},
|
||||||
|
_followee: {
|
||||||
|
host: followee.host,
|
||||||
|
inbox: isRemoteUser(followee) ? followee.inbox : undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await User.update({ _id: followee._id }, {
|
||||||
|
$inc: {
|
||||||
|
pendingReceivedFollowRequestsCount: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Publish receiveRequest event
|
||||||
|
if (isLocalUser(followee)) {
|
||||||
|
packUser(follower, followee).then(packed => event(followee._id, 'receiveFollowRequest', packed));
|
||||||
|
|
||||||
|
packUser(followee, followee, {
|
||||||
|
detail: true
|
||||||
|
}).then(packed => event(followee._id, 'meUpdated', packed));
|
||||||
|
|
||||||
|
// 通知を作成
|
||||||
|
notify(followee._id, follower._id, 'receiveFollowRequest');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLocalUser(follower) && isRemoteUser(followee)) {
|
||||||
|
const content = pack(renderFollow(follower, followee));
|
||||||
|
deliver(follower, content, followee.inbox);
|
||||||
|
}
|
||||||
|
}
|
24
src/services/following/requests/reject.ts
Normal file
24
src/services/following/requests/reject.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import User, { IUser, isRemoteUser, ILocalUser } from "../../../models/user";
|
||||||
|
import FollowRequest from "../../../models/follow-request";
|
||||||
|
import pack from '../../../remote/activitypub/renderer';
|
||||||
|
import renderFollow from '../../../remote/activitypub/renderer/follow';
|
||||||
|
import renderReject from '../../../remote/activitypub/renderer/reject';
|
||||||
|
import { deliver } from '../../../queue';
|
||||||
|
|
||||||
|
export default async function(followee: IUser, follower: IUser) {
|
||||||
|
if (isRemoteUser(follower)) {
|
||||||
|
const content = pack(renderReject(renderFollow(follower, followee)));
|
||||||
|
deliver(followee as ILocalUser, content, follower.inbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
await FollowRequest.remove({
|
||||||
|
followeeId: followee._id,
|
||||||
|
followerId: follower._id
|
||||||
|
});
|
||||||
|
|
||||||
|
User.update({ _id: followee._id }, {
|
||||||
|
$inc: {
|
||||||
|
pendingReceivedFollowRequestsCount: -1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue