local only visibility (#3254)

* local only visibility

* fix UI
This commit is contained in:
MeiMei 2018-11-16 05:47:29 +09:00 committed by syuilo
parent aaaaf2681a
commit bceb02d760
15 changed files with 139 additions and 11 deletions

View file

@ -96,6 +96,9 @@ common:
specified: "ダイレクト" specified: "ダイレクト"
specified-desc: "指定したユーザーにのみ公開" specified-desc: "指定したユーザーにのみ公開"
private: "非公開" private: "非公開"
local-public: "公開(ローカルのみ)"
local-home: "ホーム(ローカルのみ)"
local-followers: "フォロワー(ローカルのみ)"
note-placeholders: note-placeholders:
a: "今どうしてる?" a: "今どうしてる?"
@ -471,6 +474,9 @@ common/views/components/visibility-chooser.vue:
specified: "ダイレクト" specified: "ダイレクト"
specified-desc: "指定したユーザーにのみ公開" specified-desc: "指定したユーザーにのみ公開"
private: "非公開" private: "非公開"
local-public: "公開(ローカルのみ)"
local-home: "ホーム(ローカルのみ)"
local-followers: "フォロワー(ローカルのみ)"
common/views/components/trends.vue: common/views/components/trends.vue:
count: "{}人が投稿" count: "{}人が投稿"
@ -761,6 +767,7 @@ desktop/views/components/post-form.vue:
create-poll: "アンケートを作成" create-poll: "アンケートを作成"
text-remain: "残り{}文字" text-remain: "残り{}文字"
recent-tags: "最近" recent-tags: "最近"
local-only-message: "この投稿はローカルにのみ公開されます"
click-to-tagging: "クリックでタグ付け" click-to-tagging: "クリックでタグ付け"
visibility: "公開範囲" visibility: "公開範囲"
geolocation-alert: "お使いの端末は位置情報に対応していません" geolocation-alert: "お使いの端末は位置情報に対応していません"

View file

@ -19,6 +19,9 @@
<template v-if="note.visibility == 'specified'"><fa icon="envelope"/></template> <template v-if="note.visibility == 'specified'"><fa icon="envelope"/></template>
<template v-if="note.visibility == 'private'"><fa icon="lock"/></template> <template v-if="note.visibility == 'private'"><fa icon="lock"/></template>
</span> </span>
<span class="localOnly" v-if="note.localOnly == true">
<template><fa icon="heart"/></template>
</span>
</div> </div>
</header> </header>
</template> </template>
@ -115,4 +118,7 @@ export default Vue.extend({
> .visibility > .visibility
margin-left 8px margin-left 8px
> .localOnly
margin-left 4px
</style> </style>

View file

@ -35,6 +35,24 @@
<span>{{ $t('private') }}</span> <span>{{ $t('private') }}</span>
</div> </div>
</div> </div>
<div @click="choose('local-public')" :class="{ active: v == 'local-public' }">
<div><fa icon="globe"/></div>
<div>
<span>{{ $t('local-public') }}</span>
</div>
</div>
<div @click="choose('local-home')" :class="{ active: v == 'local-home' }">
<div><fa icon="home"/></div>
<div>
<span>{{ $t('local-home') }}</span>
</div>
</div>
<div @click="choose('local-followers')" :class="{ active: v == 'local-followers' }">
<div><fa icon="unlock"/></div>
<div>
<span>{{ $t('local-followers') }}</span>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>

View file

@ -20,6 +20,15 @@
<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link> <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
<span>{{ this.$t('reposted-by').substr(this.$t('reposted-by').indexOf('}') + 1) }}</span> <span>{{ this.$t('reposted-by').substr(this.$t('reposted-by').indexOf('}') + 1) }}</span>
<mk-time :time="note.createdAt"/> <mk-time :time="note.createdAt"/>
<span class="visibility" v-if="note.visibility != 'public'">
<template v-if="note.visibility == 'home'"><fa icon="home"/></template>
<template v-if="note.visibility == 'followers'"><fa icon="unlock"/></template>
<template v-if="note.visibility == 'specified'"><fa icon="envelope"/></template>
<template v-if="note.visibility == 'private'"><fa icon="lock"/></template>
</span>
<span class="localOnly" v-if="note.localOnly == true">
<template><fa icon="heart"/></template>
</span>
</div> </div>
<article> <article>
<mk-avatar class="avatar" :user="appearNote.user"/> <mk-avatar class="avatar" :user="appearNote.user"/>
@ -199,9 +208,6 @@ export default Vue.extend({
> span > span
flex-shrink 0 flex-shrink 0
&:last-of-type
margin-right 8px
.name .name
overflow hidden overflow hidden
flex-shrink 1 flex-shrink 1
@ -215,6 +221,18 @@ export default Vue.extend({
flex-shrink 0 flex-shrink 0
font-size 0.9em font-size 0.9em
> .visibility
margin-left 8px
[data-icon]
margin-right 0
> .localOnly
margin-left 4px
[data-icon]
margin-right 0
& + article & + article
padding-top 8px padding-top 8px

View file

@ -14,6 +14,7 @@
<b>{{ $t('recent-tags') }}:</b> <b>{{ $t('recent-tags') }}:</b>
<a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" :title="$t('click-to-tagging')">#{{ tag }}</a> <a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" :title="$t('click-to-tagging')">#{{ tag }}</a>
</div> </div>
<div class="local-only" v-if="this.localOnly == true">{{ $t('local-only-message') }}</div>
<input v-show="useCw" v-model="cw" :placeholder="$t('annotations')"> <input v-show="useCw" v-model="cw" :placeholder="$t('annotations')">
<div class="textarea"> <div class="textarea">
<textarea :class="{ with: (files.length != 0 || poll) }" <textarea :class="{ with: (files.length != 0 || poll) }"
@ -112,6 +113,7 @@ export default Vue.extend({
geo: null, geo: null,
visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility, visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility,
visibleUsers: [], visibleUsers: [],
localOnly: false,
autocomplete: null, autocomplete: null,
draghover: false, draghover: false,
recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'), recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
@ -363,7 +365,14 @@ export default Vue.extend({
source: this.$refs.visibilityButton source: this.$refs.visibilityButton
}); });
w.$once('chosen', v => { w.$once('chosen', v => {
const m = v.match(/^local-(.+)/);
if (m) {
this.localOnly = true;
this.visibility = m[1];
} else {
this.localOnly = false;
this.visibility = v; this.visibility = v;
}
}); });
}, },
@ -407,6 +416,7 @@ export default Vue.extend({
cw: this.useCw ? this.cw || '' : undefined, cw: this.useCw ? this.cw || '' : undefined,
visibility: this.visibility, visibility: this.visibility,
visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined, visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
localOnly: this.localOnly,
geo: this.geo ? { geo: this.geo ? {
coordinates: [this.geo.longitude, this.geo.latitude], coordinates: [this.geo.longitude, this.geo.latitude],
altitude: this.geo.altitude, altitude: this.geo.altitude,
@ -640,6 +650,10 @@ export default Vue.extend({
margin-right 8px margin-right 8px
white-space nowrap white-space nowrap
> .local-only
margin 0 0 8px 0
color var(--primary)
> .mk-uploader > .mk-uploader
margin 8px 0 0 0 margin 8px 0 0 0
padding 8px padding 8px

View file

@ -16,6 +16,15 @@
<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> <router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
<span>{{ this.$t('reposted-by').substr(this.$t('reposted-by').indexOf('}') + 1) }}</span> <span>{{ this.$t('reposted-by').substr(this.$t('reposted-by').indexOf('}') + 1) }}</span>
<mk-time :time="note.createdAt"/> <mk-time :time="note.createdAt"/>
<span class="visibility" v-if="note.visibility != 'public'">
<template v-if="note.visibility == 'home'"><fa icon="home"/></template>
<template v-if="note.visibility == 'followers'"><fa icon="unlock"/></template>
<template v-if="note.visibility == 'specified'"><fa icon="envelope"/></template>
<template v-if="note.visibility == 'private'"><fa icon="lock"/></template>
</span>
<span class="localOnly" v-if="note.localOnly == true">
<template><fa icon="heart"/></template>
</span>
</div> </div>
<article> <article>
<mk-avatar class="avatar" :user="appearNote.user" v-if="$store.state.device.postStyle != 'smart'"/> <mk-avatar class="avatar" :user="appearNote.user" v-if="$store.state.device.postStyle != 'smart'"/>
@ -163,9 +172,6 @@ export default Vue.extend({
> span > span
flex-shrink 0 flex-shrink 0
&:last-of-type
margin-right 8px
.name .name
overflow hidden overflow hidden
flex-shrink 1 flex-shrink 1
@ -179,6 +185,18 @@ export default Vue.extend({
flex-shrink 0 flex-shrink 0
font-size 0.9em font-size 0.9em
> .visibility
margin-left 8px
[data-icon]
margin-right 0
> .localOnly
margin-left 4px
[data-icon]
margin-right 0
& + article & + article
padding-top 8px padding-top 8px

View file

@ -102,6 +102,7 @@ export default Vue.extend({
geo: null, geo: null,
visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility, visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility,
visibleUsers: [], visibleUsers: [],
localOnly: false,
useCw: false, useCw: false,
cw: null, cw: null,
recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'), recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
@ -274,7 +275,14 @@ export default Vue.extend({
compact: true compact: true
}); });
w.$once('chosen', v => { w.$once('chosen', v => {
const m = v.match(/^local-(.+)/);
if (m) {
this.localOnly = true;
this.visibility = m[1];
} else {
this.localOnly = false;
this.visibility = v; this.visibility = v;
}
}); });
}, },
@ -320,6 +328,7 @@ export default Vue.extend({
} : null, } : null,
visibility: this.visibility, visibility: this.visibility,
visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined, visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
localOnly: this.localOnly,
viaMobile: viaMobile viaMobile: viaMobile
}).then(data => { }).then(data => {
this.$emit('posted'); this.$emit('posted');

View file

@ -26,6 +26,13 @@ props:
ja-JP: "モバイル端末から投稿したか否か(自己申告であることに留意)" ja-JP: "モバイル端末から投稿したか否か(自己申告であることに留意)"
en-US: "Whether this note sent via a mobile device" en-US: "Whether this note sent via a mobile device"
localOnly:
type: "boolean"
optional: true
desc:
ja-JP: "ローカルのみに公開する投稿か否か"
en-US: "Whether this note is no federation"
text: text:
type: "string" type: "string"
optional: true optional: true

View file

@ -50,6 +50,7 @@ export type INote = {
userId: mongo.ObjectID; userId: mongo.ObjectID;
appId: mongo.ObjectID; appId: mongo.ObjectID;
viaMobile: boolean; viaMobile: boolean;
localOnly: boolean;
renoteCount: number; renoteCount: number;
repliesCount: number; repliesCount: number;
reactionCounts: any; reactionCounts: any;

View file

@ -6,6 +6,8 @@ export function createHttpJob(data: any) {
} }
export function deliver(user: ILocalUser, content: any, to: any) { export function deliver(user: ILocalUser, content: any, to: any) {
if (content == null) return;
createHttpJob({ createHttpJob({
type: 'deliver', type: 'deliver',
user, user,

View file

@ -116,6 +116,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
cw: note.summary, cw: note.summary,
text: text, text: text,
viaMobile: false, viaMobile: false,
localOnly: false,
geo: undefined, geo: undefined,
visibility, visibility,
visibleUsers, visibleUsers,

View file

@ -66,7 +66,8 @@ router.get('/notes/:note', async (ctx, next) => {
const note = await Note.findOne({ const note = await Note.findOne({
_id: new mongo.ObjectID(ctx.params.note), _id: new mongo.ObjectID(ctx.params.note),
visibility: { $in: ['public', 'home'] } visibility: { $in: ['public', 'home'] },
localOnly: { $ne: true }
}); });
if (note === null) { if (note === null) {
@ -83,7 +84,8 @@ router.get('/notes/:note', async (ctx, next) => {
router.get('/notes/:note/activity', async ctx => { router.get('/notes/:note/activity', async ctx => {
const note = await Note.findOne({ const note = await Note.findOne({
_id: new mongo.ObjectID(ctx.params.note), _id: new mongo.ObjectID(ctx.params.note),
visibility: { $in: ['public', 'home'] } visibility: { $in: ['public', 'home'] },
localOnly: { $ne: true }
}); });
if (note === null) { if (note === null) {

View file

@ -55,7 +55,8 @@ export default async (ctx: Router.IRouterContext) => {
const query = { const query = {
userId: user._id, userId: user._id,
visibility: { $in: ['public', 'home'] } visibility: { $in: ['public', 'home'] },
localOnly: { $ne: true }
} as any; } as any;
if (sinceId) { if (sinceId) {

View file

@ -74,6 +74,14 @@ export const meta = {
} }
}, },
localOnly: {
validator: $.bool.optional,
default: false,
desc: {
'ja-JP': 'ローカルのみに投稿か否か。'
}
},
geo: { geo: {
validator: $.obj({ validator: $.obj({
coordinates: $.arr().length(2) coordinates: $.arr().length(2)
@ -226,6 +234,7 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => {
cw: ps.cw, cw: ps.cw,
app, app,
viaMobile: ps.viaMobile, viaMobile: ps.viaMobile,
localOnly: ps.localOnly,
visibility: ps.visibility, visibility: ps.visibility,
visibleUsers, visibleUsers,
geo: ps.geo geo: ps.geo

View file

@ -95,6 +95,7 @@ type Option = {
geo?: any; geo?: any;
poll?: any; poll?: any;
viaMobile?: boolean; viaMobile?: boolean;
localOnly?: boolean;
cw?: string; cw?: string;
visibility?: string; visibility?: string;
visibleUsers?: IUser[]; visibleUsers?: IUser[];
@ -109,6 +110,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
if (data.createdAt == null) data.createdAt = new Date(); if (data.createdAt == null) data.createdAt = new Date();
if (data.visibility == null) data.visibility = 'public'; if (data.visibility == null) data.visibility = 'public';
if (data.viaMobile == null) data.viaMobile = false; if (data.viaMobile == null) data.viaMobile = false;
if (data.localOnly == null) data.localOnly = false;
if (data.visibleUsers) { if (data.visibleUsers) {
data.visibleUsers = erase(null, data.visibleUsers); data.visibleUsers = erase(null, data.visibleUsers);
@ -139,6 +141,16 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
return rej('Renote target is private of others'); return rej('Renote target is private of others');
} }
// ローカルのみをRenoteしたらローカルのみにする
if (data.renote && data.renote.localOnly) {
data.localOnly = true;
}
// ローカルのみにリプライしたらローカルのみにする
if (data.reply && data.reply.localOnly) {
data.localOnly = true;
}
if (data.text) { if (data.text) {
data.text = data.text.trim(); data.text = data.text.trim();
} }
@ -308,6 +320,8 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
}); });
async function renderActivity(data: Option, note: INote) { async function renderActivity(data: Option, note: INote) {
if (data.localOnly) return null;
const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0) const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0)
? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote._id}`, note) ? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote._id}`, note)
: renderCreate(await renderNote(note, false), note); : renderCreate(await renderNote(note, false), note);
@ -389,6 +403,7 @@ async function insertNote(user: IUser, data: Option, tags: string[], emojis: str
emojis, emojis,
userId: user._id, userId: user._id,
viaMobile: data.viaMobile, viaMobile: data.viaMobile,
localOnly: data.localOnly,
geo: data.geo || null, geo: data.geo || null,
appId: data.app ? data.app._id : null, appId: data.app ? data.app._id : null,
visibility: data.visibility, visibility: data.visibility,