From 59d67d314069c19dcc5c2c7d82f260a9f8c661cd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 24 Sep 2018 16:26:12 +0900
Subject: [PATCH] =?UTF-8?q?=E3=83=94=E3=83=B3=E7=95=99=E3=82=81=E3=82=92?=
 =?UTF-8?q?=E8=A7=A3=E9=99=A4=E3=81=99=E3=82=8B=E3=81=93=E3=81=A8=E3=81=8C?=
 =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=97?=
 =?UTF-8?q?=E3=81=9F=E3=82=8A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/ja-JP.yml                             |  1 +
 .../app/common/views/components/note-menu.vue | 27 +++++++--
 src/server/api/endpoints/i/pin.ts             | 49 +++++++++-------
 src/server/api/endpoints/i/unpin.ts           | 57 +++++++++++++++++++
 .../api/endpoints/notes/favorites/create.ts   | 18 ++++--
 src/services/i/pin.ts                         | 22 ++-----
 6 files changed, 128 insertions(+), 46 deletions(-)
 create mode 100644 src/server/api/endpoints/i/unpin.ts

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 6ae38d45fd..db2a155221 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -328,6 +328,7 @@ common/views/components/note-menu.vue:
   copy-link: "リンクをコピー"
   favorite: "お気に入り"
   pin: "ピン留め"
+  unpin: "ピン留め解除"
   delete: "削除"
   delete-confirm: "この投稿を削除しますか?"
   remote: "投稿元で見る"
diff --git a/src/client/app/common/views/components/note-menu.vue b/src/client/app/common/views/components/note-menu.vue
index 08fae46dd6..a3e80e33de 100644
--- a/src/client/app/common/views/components/note-menu.vue
+++ b/src/client/app/common/views/components/note-menu.vue
@@ -28,11 +28,19 @@ export default Vue.extend({
 			}];
 
 			if (this.note.userId == this.$store.state.i.id) {
-				items.push({
-					icon: '%fa:thumbtack%',
-					text: '%i18n:@pin%',
-					action: this.pin
-				});
+				if (this.$store.state.i.pinnedNoteIds.includes(this.note.id)) {
+					items.push({
+						icon: '%fa:thumbtack%',
+						text: '%i18n:@unpin%',
+						action: this.unpin
+					});
+				} else {
+					items.push({
+						icon: '%fa:thumbtack%',
+						text: '%i18n:@pin%',
+						action: this.pin
+					});
+				}
 			}
 
 			if (this.note.userId == this.$store.state.i.id || this.$store.state.i.isAdmin) {
@@ -56,6 +64,7 @@ export default Vue.extend({
 			return items;
 		}
 	},
+
 	methods: {
 		detail() {
 			this.$router.push(`/notes/${ this.note.id }`);
@@ -73,6 +82,14 @@ export default Vue.extend({
 			});
 		},
 
+		unpin() {
+			(this as any).api('i/unpin', {
+				noteId: this.note.id
+			}).then(() => {
+				this.destroyDom();
+			});
+		},
+
 		del() {
 			if (!window.confirm('%i18n:@delete-confirm%')) return;
 			(this as any).api('notes/delete', {
diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts
index d075976b74..f9ae032b11 100644
--- a/src/server/api/endpoints/i/pin.ts
+++ b/src/server/api/endpoints/i/pin.ts
@@ -1,21 +1,35 @@
-import * as mongo from 'mongodb';
 import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
 import User, { ILocalUser } from '../../../../models/user';
 import Note from '../../../../models/note';
 import { pack } from '../../../../models/user';
 import { deliverPinnedChange } from '../../../../services/i/pin';
+import getParams from '../../get-params';
+
+export const meta = {
+	desc: {
+		'ja-JP': '指定した投稿をピン留めします。'
+	},
+
+	requireCredential: true,
+
+	kind: 'account-write',
+
+	params: {
+		noteId: $.type(ID).note({
+			desc: {
+				'ja-JP': '対象の投稿のID'
+			}
+		})
+	}
+};
 
-/**
- * Pin note
- */
 export default async (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'noteId' parameter
-	const [noteId, noteIdErr] = $.type(ID).get(params.noteId);
-	if (noteIdErr) return rej('invalid noteId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Fetch pinee
 	const note = await Note.findOne({
-		_id: noteId,
+		_id: ps.noteId,
 		userId: user._id
 	});
 
@@ -23,21 +37,17 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
 		return rej('note not found');
 	}
 
-	let addedId: mongo.ObjectID;
-	let removedId: mongo.ObjectID;
-
 	const pinnedNoteIds = user.pinnedNoteIds || [];
 
+	if (pinnedNoteIds.length > 5) {
+		return rej('cannot pin more notes');
+	}
+
 	if (pinnedNoteIds.some(id => id.equals(note._id))) {
 		return rej('already exists');
 	}
 
 	pinnedNoteIds.unshift(note._id);
-	addedId = note._id;
-
-	if (pinnedNoteIds.length > 5) {
-		removedId = pinnedNoteIds.pop();
-	}
 
 	await User.update(user._id, {
 		$set: {
@@ -45,14 +55,13 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
 		}
 	});
 
-	// Serialize
 	const iObj = await pack(user, user, {
 		detail: true
 	});
 
-	// Send Add/Remove to followers
-	deliverPinnedChange(user._id, removedId, addedId);
-
 	// Send response
 	res(iObj);
+
+	// Send Add to followers
+	deliverPinnedChange(user._id, note._id, true);
 });
diff --git a/src/server/api/endpoints/i/unpin.ts b/src/server/api/endpoints/i/unpin.ts
new file mode 100644
index 0000000000..82625ae5fb
--- /dev/null
+++ b/src/server/api/endpoints/i/unpin.ts
@@ -0,0 +1,57 @@
+import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
+import User, { ILocalUser } from '../../../../models/user';
+import Note from '../../../../models/note';
+import { pack } from '../../../../models/user';
+import { deliverPinnedChange } from '../../../../services/i/pin';
+import getParams from '../../get-params';
+
+export const meta = {
+	desc: {
+		'ja-JP': '指定した投稿のピン留めを解除します。'
+	},
+
+	requireCredential: true,
+
+	kind: 'account-write',
+
+	params: {
+		noteId: $.type(ID).note({
+			desc: {
+				'ja-JP': '対象の投稿のID'
+			}
+		})
+	}
+};
+
+export default async (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
+
+	// Fetch unpinee
+	const note = await Note.findOne({
+		_id: ps.noteId,
+		userId: user._id
+	});
+
+	if (note === null) {
+		return rej('note not found');
+	}
+
+	const pinnedNoteIds = (user.pinnedNoteIds || []).filter(id => !id.equals(note._id));
+
+	await User.update(user._id, {
+		$set: {
+			pinnedNoteIds: pinnedNoteIds
+		}
+	});
+
+	const iObj = await pack(user, user, {
+		detail: true
+	});
+
+	// Send response
+	res(iObj);
+
+	// Send Remove to followers
+	deliverPinnedChange(user._id, note._id, false);
+});
diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts
index daf7780abc..9aefb701ae 100644
--- a/src/server/api/endpoints/notes/favorites/create.ts
+++ b/src/server/api/endpoints/notes/favorites/create.ts
@@ -2,6 +2,7 @@ import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
 import Favorite from '../../../../../models/favorite';
 import Note from '../../../../../models/note';
 import { ILocalUser } from '../../../../../models/user';
+import getParams from '../../../get-params';
 
 export const meta = {
 	desc: {
@@ -11,17 +12,24 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'favorite-write'
+	kind: 'favorite-write',
+
+	params: {
+		noteId: $.type(ID).note({
+			desc: {
+				'ja-JP': '対象の投稿のID'
+			}
+		})
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'noteId' parameter
-	const [noteId, noteIdErr] = $.type(ID).get(params.noteId);
-	if (noteIdErr) return rej('invalid noteId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Get favoritee
 	const note = await Note.findOne({
-		_id: noteId
+		_id: ps.noteId
 	});
 
 	if (note === null) {
diff --git a/src/services/i/pin.ts b/src/services/i/pin.ts
index 5bf8d166bb..8b7287e68d 100644
--- a/src/services/i/pin.ts
+++ b/src/services/i/pin.ts
@@ -7,7 +7,7 @@ import renderRemove from '../../remote/activitypub/renderer/remove';
 import packAp from '../../remote/activitypub/renderer';
 import { deliver } from '../../queue';
 
-export async function deliverPinnedChange(userId: mongo.ObjectID, oldId?: mongo.ObjectID, newId?: mongo.ObjectID) {
+export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo.ObjectID, isAddition: boolean) {
 	const user = await User.findOne({
 		_id: userId
 	});
@@ -20,21 +20,11 @@ export async function deliverPinnedChange(userId: mongo.ObjectID, oldId?: mongo.
 
 	const target = `${config.url}/users/${user._id}/collections/featured`;
 
-	if (oldId) {
-		const oldItem = `${config.url}/notes/${oldId}`;
-		const content = packAp(renderRemove(user, target, oldItem));
-		queue.forEach(inbox => {
-			deliver(user, content, inbox);
-		});
-	}
-
-	if (newId) {
-		const newItem = `${config.url}/notes/${newId}`;
-		const content = packAp(renderAdd(user, target, newItem));
-		queue.forEach(inbox => {
-			deliver(user, content, inbox);
-		});
-	}
+	const item = `${config.url}/notes/${noteId}`;
+	const content = packAp(isAddition ? renderAdd(user, target, item) : renderRemove(user, target, item));
+	queue.forEach(inbox => {
+		deliver(user, content, inbox);
+	});
 }
 
 /**