refactor(client): Use v-model for note component, freeze object
Related: #6595
This commit is contained in:
parent
cf43dd6ec5
commit
14b7f05af4
10 changed files with 111 additions and 70 deletions
|
@ -40,14 +40,14 @@
|
||||||
<x-note-header class="header" :note="appearNote" :mini="true"/>
|
<x-note-header class="header" :note="appearNote" :mini="true"/>
|
||||||
<div class="body" ref="noteBody">
|
<div class="body" ref="noteBody">
|
||||||
<p v-if="appearNote.cw != null" class="cw">
|
<p v-if="appearNote.cw != null" class="cw">
|
||||||
<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="emojis" v-once/>
|
<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" v-once/>
|
||||||
<x-cw-button v-model="showContent" :note="appearNote"/>
|
<x-cw-button v-model="showContent" :note="appearNote"/>
|
||||||
</p>
|
</p>
|
||||||
<div class="content" v-show="appearNote.cw == null || showContent">
|
<div class="content" v-show="appearNote.cw == null || showContent">
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
|
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
|
||||||
<router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><fa :icon="faReply"/></router-link>
|
<router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><fa :icon="faReply"/></router-link>
|
||||||
<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="emojis" v-once/>
|
<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" v-once/>
|
||||||
<a class="rp" v-if="appearNote.renote != null">RN:</a>
|
<a class="rp" v-if="appearNote.renote != null">RN:</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="files" v-if="appearNote.files.length > 0">
|
<div class="files" v-if="appearNote.files.length > 0">
|
||||||
|
@ -59,7 +59,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<x-reactions-viewer :note="appearNote" :reactions="reactions" :my-reaction="myReaction" :emojis="emojis" ref="reactionsViewer"/>
|
<x-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
|
||||||
<button @click="reply()" class="button _button">
|
<button @click="reply()" class="button _button">
|
||||||
<template v-if="appearNote.reply"><fa :icon="faReplyAll"/></template>
|
<template v-if="appearNote.reply"><fa :icon="faReplyAll"/></template>
|
||||||
<template v-else><fa :icon="faReply"/></template>
|
<template v-else><fa :icon="faReply"/></template>
|
||||||
|
@ -71,10 +71,10 @@
|
||||||
<button v-else class="button _button">
|
<button v-else class="button _button">
|
||||||
<fa :icon="faBan"/>
|
<fa :icon="faBan"/>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="!isMyNote && myReaction == null" class="button _button" @click="react()" ref="reactButton">
|
<button v-if="!isMyNote && appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
|
||||||
<fa :icon="faPlus"/>
|
<fa :icon="faPlus"/>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="!isMyNote && myReaction != null" class="button _button reacted" @click="undoReact()" ref="reactButton">
|
<button v-if="!isMyNote && appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton">
|
||||||
<fa :icon="faMinus"/>
|
<fa :icon="faMinus"/>
|
||||||
</button>
|
</button>
|
||||||
<button class="button _button" @click="menu()" ref="menuButton">
|
<button class="button _button" @click="menu()" ref="menuButton">
|
||||||
|
@ -116,6 +116,11 @@ import copyToClipboard from '../scripts/copy-to-clipboard';
|
||||||
import { checkWordMute } from '../scripts/check-word-mute';
|
import { checkWordMute } from '../scripts/check-word-mute';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
model: {
|
||||||
|
prop: 'note',
|
||||||
|
event: 'updated'
|
||||||
|
},
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
XSub,
|
XSub,
|
||||||
XNoteHeader,
|
XNoteHeader,
|
||||||
|
@ -152,9 +157,6 @@ export default Vue.extend({
|
||||||
showContent: false,
|
showContent: false,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
muted: false,
|
muted: false,
|
||||||
myReaction: null,
|
|
||||||
reactions: {},
|
|
||||||
emojis: [],
|
|
||||||
noteBody: this.$refs.noteBody,
|
noteBody: this.$refs.noteBody,
|
||||||
faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug
|
faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug
|
||||||
};
|
};
|
||||||
|
@ -211,7 +213,9 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
reactionsCount(): number {
|
reactionsCount(): number {
|
||||||
return sum(Object.values(this.reactions));
|
return this.appearNote.reactions
|
||||||
|
? sum(Object.values(this.appearNote.reactions))
|
||||||
|
: 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
urls(): string[] {
|
urls(): string[] {
|
||||||
|
@ -242,9 +246,8 @@ export default Vue.extend({
|
||||||
this.connection = this.$root.stream;
|
this.connection = this.$root.stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emojis = [...this.appearNote.emojis];
|
console.log(this.note);
|
||||||
this.reactions = { ...this.appearNote.reactions };
|
|
||||||
this.myReaction = this.appearNote.myReaction;
|
|
||||||
this.muted = await checkWordMute(this.appearNote, this.$store.state.i, this.$store.state.settings.mutedWords);
|
this.muted = await checkWordMute(this.appearNote, this.$store.state.i, this.$store.state.settings.mutedWords);
|
||||||
|
|
||||||
if (this.detail) {
|
if (this.detail) {
|
||||||
|
@ -284,6 +287,19 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
updateAppearNote(v) {
|
||||||
|
this.$emit('updated', Object.freeze(this.isRenote ? {
|
||||||
|
...this.note,
|
||||||
|
renote: {
|
||||||
|
...this.note.renote,
|
||||||
|
...v
|
||||||
|
}
|
||||||
|
} : {
|
||||||
|
...this.note,
|
||||||
|
...v
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
readPromo() {
|
readPromo() {
|
||||||
(this as any).$root.api('promo/read', {
|
(this as any).$root.api('promo/read', {
|
||||||
noteId: this.appearNote.id
|
noteId: this.appearNote.id
|
||||||
|
@ -320,47 +336,83 @@ export default Vue.extend({
|
||||||
case 'reacted': {
|
case 'reacted': {
|
||||||
const reaction = body.reaction;
|
const reaction = body.reaction;
|
||||||
|
|
||||||
|
// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
|
||||||
|
let n = {
|
||||||
|
...this.appearNote,
|
||||||
|
};
|
||||||
|
|
||||||
if (body.emoji) {
|
if (body.emoji) {
|
||||||
if (!this.emojis.includes(body.emoji)) {
|
const emojis = this.appearNote.emojis || [];
|
||||||
this.emojis.push(body.emoji);
|
if (!emojis.includes(body.emoji)) {
|
||||||
|
n.emojis = [...emojis, body.emoji];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.reactions[reaction] == null) {
|
// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
|
||||||
Vue.set(this.reactions, reaction, 0);
|
const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
|
||||||
}
|
|
||||||
|
|
||||||
// Increment the count
|
// Increment the count
|
||||||
this.reactions[reaction]++;
|
n.reactions = {
|
||||||
|
...this.appearNote.reactions,
|
||||||
|
[reaction]: currentCount + 1
|
||||||
|
};
|
||||||
|
|
||||||
if (body.userId === this.$store.state.i.id) {
|
if (body.userId === this.$store.state.i.id) {
|
||||||
this.myReaction = reaction;
|
n.myReaction = reaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateAppearNote(n);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'unreacted': {
|
case 'unreacted': {
|
||||||
const reaction = body.reaction;
|
const reaction = body.reaction;
|
||||||
|
|
||||||
if (this.reactions[reaction] == null) {
|
// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
|
||||||
return;
|
let n = {
|
||||||
}
|
...this.appearNote,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
|
||||||
|
const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
|
||||||
|
|
||||||
// Decrement the count
|
// Decrement the count
|
||||||
if (this.reactions[reaction] > 0) this.reactions[reaction]--;
|
n.reactions = {
|
||||||
|
...this.appearNote.reactions,
|
||||||
|
[reaction]: Math.max(0, currentCount - 1)
|
||||||
|
};
|
||||||
|
|
||||||
if (body.userId === this.$store.state.i.id) {
|
if (body.userId === this.$store.state.i.id) {
|
||||||
this.myReaction = null;
|
n.myReaction = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateAppearNote(n);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'pollVoted': {
|
case 'pollVoted': {
|
||||||
const choice = body.choice;
|
const choice = body.choice;
|
||||||
this.appearNote.poll.choices[choice].votes++;
|
|
||||||
if (body.userId === this.$store.state.i.id) {
|
// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
|
||||||
Vue.set(this.appearNote.poll.choices[choice], 'isVoted', true);
|
let n = {
|
||||||
}
|
...this.appearNote,
|
||||||
|
};
|
||||||
|
|
||||||
|
n.poll = {
|
||||||
|
...this.appearNote.poll,
|
||||||
|
choices: {
|
||||||
|
...this.appearNote.poll.choices,
|
||||||
|
[choice]: {
|
||||||
|
...this.appearNote.poll.choices[choice],
|
||||||
|
votes: this.appearNote.poll.choices[choice].votes + 1,
|
||||||
|
...(body.userId === this.$store.state.i.id ? {
|
||||||
|
isVoted: true
|
||||||
|
} : {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.updateAppearNote(n);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -438,11 +490,11 @@ export default Vue.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
undoReact() {
|
undoReact(note) {
|
||||||
const oldReaction = this.myReaction;
|
const oldReaction = note.myReaction;
|
||||||
if (!oldReaction) return;
|
if (!oldReaction) return;
|
||||||
this.$root.api('notes/reactions/delete', {
|
this.$root.api('notes/reactions/delete', {
|
||||||
noteId: this.appearNote.id
|
noteId: note.id
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<x-list ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
|
<x-list ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
|
||||||
<x-note :note="note" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/>
|
<x-note :note="note" @updated="updated(note, $event)" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/>
|
||||||
</x-list>
|
</x-list>
|
||||||
|
|
||||||
<div v-show="more && !reversed" style="margin-top: var(--margin);">
|
<div v-show="more && !reversed" style="margin-top: var(--margin);">
|
||||||
|
@ -62,14 +62,15 @@ export default Vue.extend({
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
|
||||||
extract: {
|
prop: {
|
||||||
|
type: String,
|
||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
notes(): any[] {
|
notes(): any[] {
|
||||||
return this.extract ? this.extract(this.items) : this.items;
|
return this.prop ? this.items.map(item => item[this.prop]) : this.items;
|
||||||
},
|
},
|
||||||
|
|
||||||
reversed(): boolean {
|
reversed(): boolean {
|
||||||
|
@ -78,6 +79,15 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
updated(oldValue, newValue) {
|
||||||
|
const i = this.notes.findIndex(n => n === oldValue);
|
||||||
|
if (this.prop) {
|
||||||
|
Vue.set(this.items[i], this.prop, newValue);
|
||||||
|
} else {
|
||||||
|
Vue.set(this.items, i, newValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
this.$refs.notes.focus();
|
this.$refs.notes.focus();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
class="hkzvhatu _button"
|
class="hkzvhatu _button"
|
||||||
:class="{ reacted: myReaction == reaction, canToggle }"
|
:class="{ reacted: note.myReaction == reaction, canToggle }"
|
||||||
@click="toggleReaction(reaction)"
|
@click="toggleReaction(reaction)"
|
||||||
v-if="count > 0"
|
v-if="count > 0"
|
||||||
@touchstart="onMouseover"
|
@touchstart="onMouseover"
|
||||||
|
@ -11,7 +11,7 @@
|
||||||
ref="reaction"
|
ref="reaction"
|
||||||
v-particle="canToggle"
|
v-particle="canToggle"
|
||||||
>
|
>
|
||||||
<x-reaction-icon :reaction="reaction" :custom-emojis="emojis" ref="icon"/>
|
<x-reaction-icon :reaction="reaction" :custom-emojis="note.emojis" ref="icon"/>
|
||||||
<span>{{ count }}</span>
|
<span>{{ count }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
@ -30,14 +30,6 @@ export default Vue.extend({
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
myReaction: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
emojis: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
count: {
|
count: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -79,7 +71,7 @@ export default Vue.extend({
|
||||||
toggleReaction() {
|
toggleReaction() {
|
||||||
if (!this.canToggle) return;
|
if (!this.canToggle) return;
|
||||||
|
|
||||||
const oldReaction = this.myReaction;
|
const oldReaction = this.note.myReaction;
|
||||||
if (oldReaction) {
|
if (oldReaction) {
|
||||||
this.$root.api('notes/reactions/delete', {
|
this.$root.api('notes/reactions/delete', {
|
||||||
noteId: this.note.id
|
noteId: this.note.id
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="tdflqwzn" :class="{ isMe }">
|
<div class="tdflqwzn" :class="{ isMe }">
|
||||||
<x-reaction v-for="(count, reaction) in reactions" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note" :my-reaction="myReaction" :emojis="emojis" :key="reaction"/>
|
<x-reaction v-for="(count, reaction) in note.reactions" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note" :key="reaction"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -12,28 +12,16 @@ export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
XReaction
|
XReaction
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
initialReactions: new Set(Object.keys(this.note.reactions))
|
||||||
|
};
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
note: {
|
note: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
reactions: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
myReaction: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
emojis: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
initialReactions: new Set(Object.keys(this.note.reactions))
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isMe(): boolean {
|
isMe(): boolean {
|
||||||
|
|
|
@ -52,7 +52,6 @@ export default Vue.extend({
|
||||||
});
|
});
|
||||||
|
|
||||||
const prepend = note => {
|
const prepend = note => {
|
||||||
Object.freeze(note);
|
|
||||||
(this.$refs.tl as any).prepend(note);
|
(this.$refs.tl as any).prepend(note);
|
||||||
|
|
||||||
this.$emit('note');
|
this.$emit('note');
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div>
|
<div>
|
||||||
<portal to="icon"><fa :icon="faStar"/></portal>
|
<portal to="icon"><fa :icon="faStar"/></portal>
|
||||||
<portal to="title">{{ $t('favorites') }}</portal>
|
<portal to="title">{{ $t('favorites') }}</portal>
|
||||||
<x-notes :pagination="pagination" :detail="true" :extract="items => items.map(item => item.note)" @before="before()" @after="after()"/>
|
<x-notes :pagination="pagination" :detail="true" :prop="'note'" @before="before()" @after="after()"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -436,7 +436,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
onStatsLog(statsLog) {
|
onStatsLog(statsLog) {
|
||||||
for (const stats of statsLog.reverse()) {
|
for (const stats of [...statsLog].reverse()) {
|
||||||
this.onStats(stats);
|
this.onStats(stats);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,7 +169,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
onStatsLog(statsLog) {
|
onStatsLog(statsLog) {
|
||||||
for (const stats of statsLog.reverse()) {
|
for (const stats of [...statsLog].reverse()) {
|
||||||
this.onStats(stats);
|
this.onStats(stats);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<hr v-if="showNext"/>
|
<hr v-if="showNext"/>
|
||||||
|
|
||||||
<mk-remote-caution v-if="note.user.host != null" :href="note.url || note.uri" style="margin-bottom: var(--margin)"/>
|
<mk-remote-caution v-if="note.user.host != null" :href="note.url || note.uri" style="margin-bottom: var(--margin)"/>
|
||||||
<x-note :note="note" :key="note.id" :detail="true"/>
|
<x-note v-model="note" :key="note.id" :detail="true"/>
|
||||||
|
|
||||||
<button class="_panel _button" v-if="hasPrev && !showPrev" @click="showPrev = true" style="margin: var(--margin) auto 0 auto;"><fa :icon="faChevronDown"/></button>
|
<button class="_panel _button" v-if="hasPrev && !showPrev" @click="showPrev = true" style="margin: var(--margin) auto 0 auto;"><fa :icon="faChevronDown"/></button>
|
||||||
<hr v-if="showPrev"/>
|
<hr v-if="showPrev"/>
|
||||||
|
|
|
@ -112,10 +112,10 @@ export default class Stream extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const c of connections.filter(c => c != null)) {
|
for (const c of connections.filter(c => c != null)) {
|
||||||
c.emit(body.type, body.body);
|
c.emit(body.type, Object.freeze(body.body));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.emit(type, body);
|
this.emit(type, Object.freeze(body));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue