Compare commits

..

25 Commits

Author SHA1 Message Date
syuilo
48589e0da1 12.58.0 2020-11-17 15:04:15 +09:00
syuilo
0044d83801 nanka iroiro (#6847)
* wip

* wip

* wip

* wip

* Update ja-JP.yml

* wip

* wip

* wip
2020-11-17 14:59:15 +09:00
syuilo
50e917d232 12.57.4 2020-11-15 17:40:53 +09:00
syuilo
ccd14e0462 クリップのOGP対応 2020-11-15 17:40:49 +09:00
syuilo
d0c0104546 12.57.3 2020-11-15 17:35:44 +09:00
syuilo
cd34ade638 非ログイン時にクリップを取得できない問題を修正 2020-11-15 17:35:40 +09:00
syuilo
3f91e33a8c 12.57.2 2020-11-15 17:32:36 +09:00
syuilo
17cc996288 他人のpublicなクリップを取得できない問題を修正 2020-11-15 17:32:29 +09:00
syuilo
385776dc0f Update config.yml 2020-11-15 16:06:18 +09:00
syuilo
e52278c371 12.57.1 2020-11-15 16:04:21 +09:00
syuilo
7ffc8c1eda Update config.yml
https://support.circleci.com/hc/en-us/articles/360050934711
2020-11-15 16:04:08 +09:00
syuilo
1359615c82 Add missing translations 2020-11-15 14:18:35 +09:00
syuilo
7a7a56940c 一旦パブリックにしないとクリップ作成できない問題を修正 2020-11-15 14:18:25 +09:00
syuilo
bcbe83cb38 12.57.0 2020-11-15 13:50:02 +09:00
syuilo
37f983aee3 New Crowdin updates (#6838)
* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Kabyle)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (French)
2020-11-15 13:49:38 +09:00
syuilo
77de3f2b9d Pages埋め込みノートで詳細表示にするかどうか選べるように 2020-11-15 13:47:15 +09:00
syuilo
f655b54937 Improve Pages
Resolve #6654
2020-11-15 13:42:04 +09:00
syuilo
cac99ebdd4 wip: clip 2020-11-15 12:47:54 +09:00
takenoko
f0d0a1546a Use node v14.15.0 (#6837)
* Use node v14.15.0

* Update Dockerfile
2020-11-15 12:35:28 +09:00
syuilo
8e8459fa55 wip: clip 2020-11-15 12:34:47 +09:00
syuilo
d53c55ecb5 wip: clip 2020-11-15 12:04:54 +09:00
syuilo
ea33d61a90 Add description 2020-11-15 10:35:36 +09:00
syuilo
2fcc3388dd Update about-misskey.vue 2020-11-15 10:21:56 +09:00
rinsuki
ef7f033c32 CI: Node 15.x でも回すように (#6836) 2020-11-14 15:13:59 +09:00
syuilo
7aa54dc92e Update Dockerfile 2020-11-14 15:12:43 +09:00
70 changed files with 1751 additions and 261 deletions

View File

@@ -15,7 +15,8 @@ jobs:
executor: docker
steps:
- checkout
- setup_remote_docker
- setup_remote_docker:
version: 19.03.13
- run:
name: Build
command: |

View File

@@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
node-version: [12.x, 14.x]
node-version: [12.x, 14.x, 15.x]
services:
postgres:

View File

@@ -1 +1 @@
v15.0.1
v14.15.0

View File

@@ -1,9 +1,7 @@
FROM node:15.0.1-alpine AS base
FROM node:14.15.0-alpine AS base
ENV NODE_ENV=production
RUN npm i -g npm@latest
WORKDIR /misskey
FROM base AS builder

View File

@@ -174,6 +174,7 @@ imageUrl: "عنوان URL للصورة"
remove: "حذف"
removed: "تم حذفه بنجاح"
removeAreYouSure: "متأكد من أنك تريد حذف {x}؟"
deleteAreYouSure: "متأكد من أنك تريد حذف {x}؟"
saved: "تم حفظه"
messaging: "الدردشة"
upload: "تحميل"

View File

@@ -214,6 +214,8 @@ imageUrl: "Bild-URL"
remove: "Löschen"
removed: "Erfolgreich gelöscht"
removeAreYouSure: "Möchtest du \"{x}\" wirklich löschen?"
deleteAreYouSure: "Möchtest du \"{x}\" wirklich löschen?"
resetAreYouSure: "Wirklich zurücksetzen?"
saved: "Gespeichert"
messaging: "Chat"
upload: "Hochladen"
@@ -1079,6 +1081,7 @@ _pages:
my: "Meine Seiten"
liked: "Seiten, die mir gefallen"
inspector: "Inspektor"
contents: "Inhalt"
content: "Inhalt"
variables: "Variablen"
title: "Titel"

View File

@@ -214,6 +214,8 @@ imageUrl: "Image URL"
remove: "Delete"
removed: "Successfully deleted"
removeAreYouSure: "Are you sure that you want to delete \"{x}\"?"
deleteAreYouSure: "Are you sure that you want to delete \"{x}\"?"
resetAreYouSure: "Really reset?"
saved: "Saved"
messaging: "Messaging"
upload: "Upload"
@@ -1079,6 +1081,7 @@ _pages:
my: "My pages"
liked: "Liked pages"
inspector: "Inspector"
contents: "Content"
content: "Page block"
variables: "Variables"
title: "Title"

View File

@@ -214,6 +214,7 @@ imageUrl: "URL de la imágen"
remove: "Borrar"
removed: "Borrado"
removeAreYouSure: "¿Desea borrar \"{x}\"?"
deleteAreYouSure: "¿Desea borrar \"{x}\"?"
saved: "Guardado"
messaging: "Chat"
upload: "Subir"
@@ -1048,6 +1049,7 @@ _pages:
my: "Mis páginas"
liked: "Páginas que me gustan"
inspector: "Inspector"
contents: "Contenido"
content: "Bloque de página"
variables: "Variables"
title: "Título"

View File

@@ -213,6 +213,7 @@ imageUrl: "URL de limage"
remove: "Supprimer"
removed: "Supprimé"
removeAreYouSure: "Supprimer «{x}» ?"
deleteAreYouSure: "Supprimer «{x}» ?"
saved: "Enregistré"
messaging: "Discuter"
upload: "Téléverser"
@@ -939,6 +940,7 @@ _pages:
my: "Mes pages"
liked: "Pages favorites"
inspector: "Inspecteur"
contents: "Contenu"
content: "Bloc de page"
variables: "Variables"
title: "Titre"

View File

@@ -95,6 +95,7 @@ sensitive: "閲覧注意"
add: "追加"
reaction: "リアクション"
reactionSettingDescription: "リアクションピッカーに表示するリアクションを設定します。"
reactionSettingDescription2: "ドラッグして並び替えます。クリックして削除します。"
rememberNoteVisibility: "公開範囲を記憶する"
attachCancel: "添付取り消し"
markAsSensitive: "閲覧注意にする"
@@ -214,6 +215,7 @@ imageUrl: "画像URL"
remove: "削除"
removed: "削除しました"
removeAreYouSure: "「{x}」を削除しますか?"
deleteAreYouSure: "「{x}」を削除しますか?"
resetAreYouSure: "リセットしますか?"
saved: "保存しました"
messaging: "チャット"
@@ -314,6 +316,8 @@ bannerUrl: "バナー画像のURL"
basicInfo: "基本情報"
pinnedUsers: "ピン留めユーザー"
pinnedUsersDescription: "「みつける」ページなどにピン留めしたいユーザーを改行で区切って記述します。"
pinnedPages: "ピン留めページ"
pinnedPagesDescription: "インスタンスのトップページにピン留めしたいページのパスを改行で区切って記述します。"
hcaptcha: "hCaptcha"
enableHcaptcha: "hCaptchaを有効にする"
hcaptchaSiteKey: "サイトキー"
@@ -604,6 +608,11 @@ random: "ランダム"
system: "システム"
switchUi: "UI切り替え"
desktop: "デスクトップ"
clip: "クリップ"
createNew: "新規作成"
optional: "任意"
createNewClip: "新しいクリップを作成"
public: "パブリック"
_mfm:
cheatSheet: "MFMチートシート"
@@ -1099,6 +1108,7 @@ _pages:
created: "ページを作成しました"
updated: "ページを更新しました"
deleted: "ページを削除しました"
pageSetting: "ページ設定"
nameAlreadyExists: "指定されたページURLは既に存在しています"
invalidNameTitle: "不正なページURLです"
invalidNameText: "空白でないか確認してください"
@@ -1109,7 +1119,9 @@ _pages:
unlike: "いいね解除"
my: "自分のページ"
liked: "いいねしたページ"
featured: "人気"
inspector: "インスペクター"
contents: "コンテンツ"
content: "ページブロック"
variables: "変数"
title: "タイトル"
@@ -1170,6 +1182,12 @@ _pages:
width: "幅"
height: "高さ"
note: "ノート埋め込み"
_note:
id: "ートID"
idDescription: "ートURLをペーストして設定することもできます。"
detailed: "詳細な表示"
switch: "スイッチ"
_switch:
name: "変数名"

View File

@@ -214,6 +214,7 @@ imageUrl: "画像URL"
remove: "ほかす"
removed: "削除したで!"
removeAreYouSure: "「{x}」はなおしてしもてええか?"
deleteAreYouSure: "「{x}」はなおしてしもてええか?"
saved: "保存したで!"
messaging: "チャット"
upload: "アップロード"

View File

@@ -57,6 +57,7 @@ _exportOrImport:
blockingList: "Seḥbes"
userLists: "Tibdarin"
_pages:
contents: "Agbur"
font: "Tasefsit"
fontSerif: "Serif"
fontSansSerif: "Sans Serif"

View File

@@ -204,6 +204,7 @@ imageUrl: "이미지 URL"
remove: "삭제"
removed: "삭제하였습니다"
removeAreYouSure: "\"{x}\" 을(를) 삭제하시겠습니까?"
deleteAreYouSure: "\"{x}\" 을(를) 삭제하시겠습니까?"
saved: "저장하였습니다"
messaging: "대화"
upload: "업로드"
@@ -888,6 +889,7 @@ _pages:
my: "내 페이지"
liked: "좋아요한 페이지"
inspector: "인스펙터"
contents: "콘텐츠"
content: "페이지 블록"
variables: "변수"
title: "제목"

View File

@@ -214,6 +214,8 @@ imageUrl: "Ссылка на изображение"
remove: "Удалить"
removed: "Удалено"
removeAreYouSure: "Хотите удалить «{x}»?"
deleteAreYouSure: "Хотите удалить «{x}»?"
resetAreYouSure: "На самом деле сбросить?"
saved: "Сохранено"
messaging: "Сообщения"
upload: "Загрузить"
@@ -1079,6 +1081,7 @@ _pages:
my: "Свои страницы"
liked: "Понравившиеся страницы"
inspector: "Инспектор"
contents: "Содержательные"
content: "Содержимое"
variables: "Переменные"
title: "Заголовок"

View File

@@ -380,7 +380,21 @@ promotion: "Просування"
promote: "Просунути"
numberOfDays: "Кількість днів"
hideThisNote: "Сховати цей допис"
objectStorageBaseUrl: "Base URL"
objectStorageBucket: "Bucket"
objectStoragePrefix: "Prefix"
objectStorageEndpoint: "Endpoint"
objectStorageRegion: "Region"
objectStorageUseSSL: "Використовувати SSL"
objectStorageUseProxy: "Використовувати Proxy"
deleteAll: "Видалити все"
newNoteRecived: "Є нові дописи"
sounds: "Звук"
listen: "Слухати"
showInPage: "Показати на сторінці"
volume: "Гучність"
install: "Інсталювати"
uninstall: "Видалити"
sort: "Сортування"
ascendingOrder: "За зростанням"
descendingOrder: "За спаданням"

View File

@@ -214,6 +214,8 @@ imageUrl: "图片URL"
remove: "删除"
removed: "已删除"
removeAreYouSure: "要删掉「{x}」吗?"
deleteAreYouSure: "要删掉「{x}」吗?"
resetAreYouSure: "恢复默认设置?"
saved: "已保存"
messaging: "聊天"
upload: "上传"
@@ -1079,6 +1081,7 @@ _pages:
my: "我的页面"
liked: "喜欢的页面"
inspector: "检查器"
contents: "内容"
content: "页面内容"
variables: "变量"
title: "标题"

View File

@@ -212,6 +212,7 @@ imageUrl: "圖片URL"
remove: "刪除"
removed: "成功移除"
removeAreYouSure: "確定要刪掉「{x}」嗎?"
deleteAreYouSure: "確定要刪掉「{x}」嗎?"
saved: "已保存"
messaging: "傳送訊息"
upload: "上傳"

View File

@@ -0,0 +1,15 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class clipDescription1605408848373 implements MigrationInterface {
name = 'clipDescription1605408848373'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "clip" ADD "description" character varying(2048) DEFAULT null`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "clip" DROP COLUMN "description"`);
}
}

View File

@@ -0,0 +1,434 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class comments1605408971051 implements MigrationInterface {
name = 'comments1605408971051'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`COMMENT ON COLUMN "log"."createdAt" IS 'The created date of the Log.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."createdAt" IS 'The created date of the DriveFolder.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."name" IS 'The name of the DriveFolder.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."userId" IS 'The owner ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."parentId" IS 'The parent folder ID. If null, it means the DriveFolder is located in root.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."createdAt" IS 'The created date of the DriveFile.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."userId" IS 'The owner ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."userHost" IS 'The host of owner. It will be null if the user in local.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."md5" IS 'The MD5 hash of the DriveFile.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."name" IS 'The file name of the DriveFile.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."type" IS 'The content type (MIME) of the DriveFile.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."size" IS 'The file size (bytes) of the DriveFile.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."comment" IS 'The comment of the DriveFile.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."blurhash" IS 'The BlurHash string.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."properties" IS 'The any properties of the DriveFile. For example, it includes image width/height.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS 'The URL of the DriveFile.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."thumbnailUrl" IS 'The URL of the thumbnail of the DriveFile.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."webpublicUrl" IS 'The URL of the webpublic of the DriveFile.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS 'The URI of the DriveFile. it will be null when the DriveFile is local.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."folderId" IS 'The parent folder ID. If null, it means the DriveFile is located in root.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."isSensitive" IS 'Whether the DriveFile is NSFW.'`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."isLink" IS 'Whether the DriveFile is direct link to remote server.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."createdAt" IS 'The created date of the User.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."updatedAt" IS 'The updated date of the User.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."username" IS 'The username of the User.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."usernameLower" IS 'The username (lowercased) of the User.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."name" IS 'The name of the User.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."followersCount" IS 'The count of followers.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."followingCount" IS 'The count of following.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."notesCount" IS 'The count of notes.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."avatarId" IS 'The ID of avatar DriveFile.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."bannerId" IS 'The ID of banner DriveFile.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."isSuspended" IS 'Whether the User is suspended.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."isSilenced" IS 'Whether the User is silenced.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."isLocked" IS 'Whether the User is locked.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."isBot" IS 'Whether the User is a bot.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."isCat" IS 'Whether the User is a cat.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."isAdmin" IS 'Whether the User is the admin.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."isModerator" IS 'Whether the User is a moderator.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."host" IS 'The host of the User. It will be null if the origin of the user is local.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."inbox" IS 'The inbox URL of the User. It will be null if the origin of the user is local.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."sharedInbox" IS 'The sharedInbox URL of the User. It will be null if the origin of the user is local.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."featured" IS 'The featured URL of the User. It will be null if the origin of the user is local.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."uri" IS 'The URI of the User. It will be null if the origin of the user is local.'`);
await queryRunner.query(`COMMENT ON COLUMN "user"."token" IS 'The native access token of the User. It will be null if the origin of the user is local.'`);
await queryRunner.query(`COMMENT ON COLUMN "app"."createdAt" IS 'The created date of the App.'`);
await queryRunner.query(`COMMENT ON COLUMN "app"."userId" IS 'The owner ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "app"."secret" IS 'The secret key of the App.'`);
await queryRunner.query(`COMMENT ON COLUMN "app"."name" IS 'The name of the App.'`);
await queryRunner.query(`COMMENT ON COLUMN "app"."description" IS 'The description of the App.'`);
await queryRunner.query(`COMMENT ON COLUMN "app"."permission" IS 'The permission of the App.'`);
await queryRunner.query(`COMMENT ON COLUMN "app"."callbackUrl" IS 'The callbackUrl of the App.'`);
await queryRunner.query(`COMMENT ON COLUMN "access_token"."createdAt" IS 'The created date of the AccessToken.'`);
await queryRunner.query(`COMMENT ON COLUMN "access_token"."lastUsedAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "access_token"."session" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "access_token"."appId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "access_token"."name" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "access_token"."description" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "access_token"."iconUrl" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "channel"."createdAt" IS 'The created date of the Channel.'`);
await queryRunner.query(`COMMENT ON COLUMN "channel"."userId" IS 'The owner ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "channel"."name" IS 'The name of the Channel.'`);
await queryRunner.query(`COMMENT ON COLUMN "channel"."description" IS 'The description of the Channel.'`);
await queryRunner.query(`COMMENT ON COLUMN "channel"."bannerId" IS 'The ID of banner Channel.'`);
await queryRunner.query(`COMMENT ON COLUMN "channel"."notesCount" IS 'The count of notes.'`);
await queryRunner.query(`COMMENT ON COLUMN "channel"."usersCount" IS 'The count of users.'`);
await queryRunner.query(`COMMENT ON COLUMN "note"."createdAt" IS 'The created date of the Note.'`);
await queryRunner.query(`COMMENT ON COLUMN "note"."replyId" IS 'The ID of reply target.'`);
await queryRunner.query(`COMMENT ON COLUMN "note"."renoteId" IS 'The ID of renote target.'`);
await queryRunner.query(`COMMENT ON COLUMN "note"."userId" IS 'The ID of author.'`);
await queryRunner.query(`COMMENT ON COLUMN "note"."uri" IS 'The URI of a note. it will be null when the note is local.'`);
await queryRunner.query(`COMMENT ON COLUMN "note"."url" IS 'The human readable url of a note. it will be null when the note is local.'`);
await queryRunner.query(`COMMENT ON COLUMN "note"."channelId" IS 'The ID of source channel.'`);
await queryRunner.query(`COMMENT ON COLUMN "note"."userHost" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "note"."replyUserId" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "note"."replyUserHost" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "note"."renoteUserId" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "note"."renoteUserHost" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "poll_vote"."createdAt" IS 'The created date of the PollVote.'`);
await queryRunner.query(`COMMENT ON COLUMN "note_reaction"."createdAt" IS 'The created date of the NoteReaction.'`);
await queryRunner.query(`COMMENT ON COLUMN "note_watching"."createdAt" IS 'The created date of the NoteWatching.'`);
await queryRunner.query(`COMMENT ON COLUMN "note_watching"."userId" IS 'The watcher ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "note_watching"."noteId" IS 'The target Note ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "note_watching"."noteUserId" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "note_unread"."noteUserId" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "note_unread"."noteChannelId" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."createdAt" IS 'The created date of the FollowRequest.'`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeId" IS 'The followee user ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerId" IS 'The follower user ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."requestId" IS 'id of Follow Activity.'`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerHost" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerInbox" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerSharedInbox" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeHost" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeInbox" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeSharedInbox" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "user_group"."createdAt" IS 'The created date of the UserGroup.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_group"."userId" IS 'The ID of owner.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."createdAt" IS 'The created date of the UserGroupInvitation.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."userId" IS 'The user ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."userGroupId" IS 'The group ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "notification"."createdAt" IS 'The created date of the Notification.'`);
await queryRunner.query(`COMMENT ON COLUMN "notification"."notifieeId" IS 'The ID of recipient user of the Notification.'`);
await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the Notification is read.'`);
await queryRunner.query(`COMMENT ON COLUMN "meta"."localDriveCapacityMb" IS 'Drive capacity of a local user (MB)'`);
await queryRunner.query(`COMMENT ON COLUMN "meta"."remoteDriveCapacityMb" IS 'Drive capacity of a remote user (MB)'`);
await queryRunner.query(`COMMENT ON COLUMN "meta"."maxNoteTextLength" IS 'Max allowed note text length in characters'`);
await queryRunner.query(`COMMENT ON COLUMN "following"."createdAt" IS 'The created date of the Following.'`);
await queryRunner.query(`COMMENT ON COLUMN "following"."followeeId" IS 'The followee user ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "following"."followerId" IS 'The follower user ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "following"."followerHost" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "following"."followerInbox" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "following"."followerSharedInbox" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "following"."followeeHost" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "following"."followeeInbox" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "following"."followeeSharedInbox" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."caughtAt" IS 'The caught date of the Instance.'`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."host" IS 'The host of the Instance.'`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."usersCount" IS 'The count of the users of the Instance.'`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."notesCount" IS 'The count of the notes of the Instance.'`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."softwareName" IS 'The software of the Instance.'`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."softwareVersion" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."openRegistrations" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."name" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."description" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."maintainerName" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."maintainerEmail" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."iconUrl" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."faviconUrl" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."themeColor" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "muting"."createdAt" IS 'The created date of the Muting.'`);
await queryRunner.query(`COMMENT ON COLUMN "muting"."muteeId" IS 'The mutee user ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "muting"."muterId" IS 'The muter user ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "blocking"."createdAt" IS 'The created date of the Blocking.'`);
await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockeeId" IS 'The blockee user ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockerId" IS 'The blocker user ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_list"."createdAt" IS 'The created date of the UserList.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_list"."userId" IS 'The owner ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_list"."name" IS 'The name of the UserList.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."createdAt" IS 'The created date of the UserListJoining.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."userId" IS 'The user ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."userListId" IS 'The list ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."createdAt" IS 'The created date of the UserGroupJoining.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."userId" IS 'The user ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."userGroupId" IS 'The group ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "note_favorite"."createdAt" IS 'The created date of the NoteFavorite.'`);
await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."createdAt" IS 'The created date of the AbuseUserReport.'`);
await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."targetUserHost" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."reporterHost" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."createdAt" IS 'The created date of the MessagingMessage.'`);
await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."userId" IS 'The sender user ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."groupId" IS 'The recipient group ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "signin"."createdAt" IS 'The created date of the Signin.'`);
await queryRunner.query(`COMMENT ON COLUMN "auth_session"."createdAt" IS 'The created date of the AuthSession.'`);
await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."createdAt" IS 'The created date of the ReversiGame.'`);
await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."startedAt" IS 'The started date of the ReversiGame.'`);
await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."form1" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."form2" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "reversi_matching"."createdAt" IS 'The created date of the ReversiMatching.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_note_pining"."createdAt" IS 'The created date of the UserNotePinings.'`);
await queryRunner.query(`COMMENT ON COLUMN "poll"."noteId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "poll"."noteVisibility" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "poll"."userId" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "poll"."userHost" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "user_keypair"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_publickey"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "page"."createdAt" IS 'The created date of the Page.'`);
await queryRunner.query(`COMMENT ON COLUMN "page"."updatedAt" IS 'The updated date of the Page.'`);
await queryRunner.query(`COMMENT ON COLUMN "page"."userId" IS 'The ID of author.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."location" IS 'The location of the User.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."birthday" IS 'The birthday (YYYY-MM-DD) of the User.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."description" IS 'The description (bio) of the User.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."url" IS 'Remote URL of the user.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."email" IS 'The email address of the User.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."password" IS 'The password hash of the User. It will be null if the origin of the user is local.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."clientData" IS 'The client-specific data of the User.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."room" IS 'The room data of the User.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."userHost" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."id" IS 'Variable-length id given to navigator.credentials.get()'`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."publicKey" IS 'Variable-length public key used to verify attestations (hex-encoded).'`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."lastUsed" IS 'The date of the last time the UserSecurityKey was successfully validated.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."name" IS 'User-defined name for this key'`);
await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."challenge" IS 'Hex-encoded sha256 hash of the challenge.'`);
await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."createdAt" IS 'The date challenge was created for expiry purposes.'`);
await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."registrationChallenge" IS 'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.'`);
await queryRunner.query(`COMMENT ON COLUMN "moderation_log"."createdAt" IS 'The created date of the ModerationLog.'`);
await queryRunner.query(`COMMENT ON COLUMN "announcement"."createdAt" IS 'The created date of the Announcement.'`);
await queryRunner.query(`COMMENT ON COLUMN "announcement"."updatedAt" IS 'The updated date of the Announcement.'`);
await queryRunner.query(`COMMENT ON COLUMN "announcement_read"."createdAt" IS 'The created date of the AnnouncementRead.'`);
await queryRunner.query(`COMMENT ON COLUMN "clip"."createdAt" IS 'The created date of the Clip.'`);
await queryRunner.query(`COMMENT ON COLUMN "clip"."userId" IS 'The owner ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "clip"."name" IS 'The name of the Clip.'`);
await queryRunner.query(`COMMENT ON COLUMN "clip"."description" IS 'The description of the Clip.'`);
await queryRunner.query(`COMMENT ON COLUMN "clip_note"."noteId" IS 'The note ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "clip_note"."clipId" IS 'The clip ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "antenna"."createdAt" IS 'The created date of the Antenna.'`);
await queryRunner.query(`COMMENT ON COLUMN "antenna"."userId" IS 'The owner ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "antenna"."name" IS 'The name of the Antenna.'`);
await queryRunner.query(`COMMENT ON COLUMN "antenna_note"."noteId" IS 'The note ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "antenna_note"."antennaId" IS 'The antenna ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "promo_note"."noteId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "promo_note"."userId" IS '[Denormalized]'`);
await queryRunner.query(`COMMENT ON COLUMN "promo_read"."createdAt" IS 'The created date of the PromoRead.'`);
await queryRunner.query(`COMMENT ON COLUMN "muted_note"."noteId" IS 'The note ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "muted_note"."userId" IS 'The user ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "muted_note"."reason" IS 'The reason of the MutedNote.'`);
await queryRunner.query(`COMMENT ON COLUMN "channel_following"."createdAt" IS 'The created date of the ChannelFollowing.'`);
await queryRunner.query(`COMMENT ON COLUMN "channel_following"."followeeId" IS 'The followee channel ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "channel_following"."followerId" IS 'The follower user ID.'`);
await queryRunner.query(`COMMENT ON COLUMN "channel_note_pining"."createdAt" IS 'The created date of the ChannelNotePining.'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`COMMENT ON COLUMN "channel_note_pining"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "channel_following"."followerId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "channel_following"."followeeId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "channel_following"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "muted_note"."reason" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "muted_note"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "muted_note"."noteId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "promo_read"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "promo_note"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "promo_note"."noteId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "antenna_note"."antennaId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "antenna_note"."noteId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "antenna"."name" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "antenna"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "antenna"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "clip_note"."clipId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "clip_note"."noteId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "clip"."description" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "clip"."name" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "clip"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "clip"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "announcement_read"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "announcement"."updatedAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "announcement"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "moderation_log"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."registrationChallenge" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."challenge" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."name" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."lastUsed" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."publicKey" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."id" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."userHost" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."room" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."clientData" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."password" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."email" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."url" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."description" IS 'The description (bio) of the User.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."birthday" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."location" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "page"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "page"."updatedAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "page"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_publickey"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_keypair"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "poll"."userHost" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "poll"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "poll"."noteVisibility" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "poll"."noteId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_note_pining"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "reversi_matching"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."form2" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."form1" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."startedAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "auth_session"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "signin"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."groupId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."reporterHost" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."targetUserHost" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note_favorite"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."userGroupId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."userListId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_list"."name" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_list"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_list"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockerId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockeeId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "blocking"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "muting"."muterId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "muting"."muteeId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "muting"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."themeColor" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."faviconUrl" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."iconUrl" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."maintainerEmail" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."maintainerName" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."description" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."name" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."openRegistrations" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."softwareVersion" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."softwareName" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."notesCount" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."usersCount" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."host" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "instance"."caughtAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "following"."followeeSharedInbox" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "following"."followeeInbox" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "following"."followeeHost" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "following"."followerSharedInbox" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "following"."followerInbox" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "following"."followerHost" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "following"."followerId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "following"."followeeId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "following"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "meta"."maxNoteTextLength" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "meta"."remoteDriveCapacityMb" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "meta"."localDriveCapacityMb" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "notification"."notifieeId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "notification"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."userGroupId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_group"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user_group"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeSharedInbox" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeInbox" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeHost" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerSharedInbox" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerInbox" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerHost" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."requestId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "follow_request"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note_unread"."noteChannelId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note_unread"."noteUserId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note_watching"."noteUserId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note_watching"."noteId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note_watching"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note_watching"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note_reaction"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "poll_vote"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note"."renoteUserHost" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note"."renoteUserId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note"."replyUserHost" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note"."replyUserId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note"."userHost" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note"."channelId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note"."url" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note"."uri" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note"."renoteId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note"."replyId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "note"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "channel"."usersCount" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "channel"."notesCount" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "channel"."bannerId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "channel"."description" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "channel"."name" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "channel"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "channel"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "access_token"."iconUrl" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "access_token"."description" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "access_token"."name" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "access_token"."appId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "access_token"."session" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "access_token"."lastUsedAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "access_token"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "app"."callbackUrl" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "app"."permission" IS 'The permission of the App.'`);
await queryRunner.query(`COMMENT ON COLUMN "app"."description" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "app"."name" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "app"."secret" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "app"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "app"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."token" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."uri" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."featured" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."sharedInbox" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."inbox" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."host" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."isModerator" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."isAdmin" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."isCat" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."isBot" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."isLocked" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."isSilenced" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."isSuspended" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."bannerId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."avatarId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."notesCount" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."followingCount" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."followersCount" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."name" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."usernameLower" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."username" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."updatedAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "user"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."isLink" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."isSensitive" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."folderId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."webpublicUrl" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."thumbnailUrl" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."properties" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."blurhash" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."comment" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."size" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."type" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."name" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."md5" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."userHost" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."parentId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."userId" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."name" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."createdAt" IS NULL`);
await queryRunner.query(`COMMENT ON COLUMN "log"."createdAt" IS NULL`);
}
}

View File

@@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class instancePinnedPages1605585339718 implements MigrationInterface {
name = 'instancePinnedPages1605585339718'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedPages" character varying(512) array NOT NULL DEFAULT '{"/announcements", "/featured", "/channels", "/pages", "/explore", "/games/reversi", "/about-misskey"}'::varchar[]`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedPages"`);
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.56.0",
"version": "12.58.0",
"codename": "indigo",
"repository": {
"type": "git",

View File

@@ -15,15 +15,15 @@
<div class="xkpnjxcv _section">
<label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item">
<MkInput v-if="form[item].type === 'number'" v-model:value="values[item]" type="number" :step="form[item].step || 1">
<span v-text="form[item].label || item"></span>
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</MkInput>
<MkInput v-else-if="form[item].type === 'string' && !item.multiline" v-model:value="values[item]" type="text">
<span v-text="form[item].label || item"></span>
<MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model:value="values[item]" type="text">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</MkInput>
<MkTextarea v-else-if="form[item].type === 'string' && item.multiline" v-model:value="values[item]">
<span v-text="form[item].label || item"></span>
<MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model:value="values[item]">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</MkTextarea>
<MkSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]">

View File

@@ -102,7 +102,7 @@
<script lang="ts">
import { computed, defineAsyncComponent, defineComponent, markRaw, ref } from 'vue';
import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug, faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug, faExclamationCircle, faPaperclip } from '@fortawesome/free-solid-svg-icons';
import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
import { parse } from '../../mfm/parse';
import { sum, unique } from '../../prelude/array';
@@ -610,6 +610,11 @@ export default defineComponent({
text: this.$t('favorite'),
action: () => this.toggleFavorite(true)
}),
{
icon: faPaperclip,
text: this.$t('clip'),
action: () => this.clip()
},
(this.appearNote.userId != this.$store.state.i.id) ? statePromise.then(state => state.isWatching ? {
icon: faEyeSlash,
text: this.$t('unwatch'),
@@ -762,6 +767,44 @@ export default defineComponent({
});
},
async clip() {
const clips = await os.api('clips/list');
os.modalMenu([{
icon: faPlus,
text: this.$t('createNew'),
action: async () => {
const { canceled, result } = await os.form(this.$t('createNewClip'), {
name: {
type: 'string',
label: this.$t('name')
},
description: {
type: 'string',
required: false,
multiline: true,
label: this.$t('description')
},
isPublic: {
type: 'boolean',
label: this.$t('public'),
default: false
}
});
if (canceled) return;
const clip = await os.apiWithDialog('clips/create', result);
os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
}
}, null, ...clips.map(clip => ({
text: clip.name,
action: () => {
os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
}
}))], this.$refs.menuButton, {
}).then(this.focus);
},
async promote() {
const { canceled, result: days } = await os.dialog({
title: this.$t('numberOfDays'),

View File

@@ -8,7 +8,7 @@
<MkError v-if="error" @retry="init()"/>
<div v-show="more && reversed" style="margin-bottom: var(--margin);">
<button class="_loadMore" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<button class="_loadMore" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><MkLoading inline/></template>
</button>

View File

@@ -1,5 +1,5 @@
<template>
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1">
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj _panel" tabindex="-1">
<div class="thumbnail" v-if="page.eyeCatchingImage" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
<article>
<header>
@@ -35,16 +35,11 @@ export default defineComponent({
<style lang="scss" scoped>
.vhpxefrj {
display: block;
overflow: hidden;
width: 100%;
border: solid var(--lineWidth) var(--urlPreviewBorder);
border-radius: 4px;
overflow: hidden;
border: 1px solid var(--divider);
&:hover {
text-decoration: none;
border-color: var(--urlPreviewBorderHover);
color: var(--accent);
}
> .thumbnail {

View File

@@ -18,10 +18,11 @@ import XPost from './page.post.vue';
import XCounter from './page.counter.vue';
import XRadioButton from './page.radio-button.vue';
import XCanvas from './page.canvas.vue';
import XNote from './page.note.vue';
export default defineComponent({
components: {
XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton, XCanvas
XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton, XCanvas, XNote
},
props: {
value: {

View File

@@ -0,0 +1,39 @@
<template>
<div class="voxdxuby">
<XNote v-if="note" v-model:note="note" :key="note.id" :detail="value.detailed"/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import XNote from '@/components/note.vue';
import * as os from '@/os';
export default defineComponent({
components: {
XNote
},
props: {
value: {
required: true
},
hpml: {
required: true
}
},
data() {
return {
note: null,
};
},
async mounted() {
this.note = await os.api('notes/show', { noteId: this.value.note });
}
});
</script>
<style lang="scss" scoped>
.voxdxuby {
margin: 1em 0;
}
</style>

View File

@@ -1,26 +1,32 @@
<template>
<div class="pxhvhrfw" v-size="{ max: [500] }">
<button v-for="item in items" class="_button" @click="$emit('update:value', item.value)" :class="{ active: value === item.value }" :disabled="value === item.value" :key="item.value"><Fa v-if="item.icon" :icon="item.icon" class="icon"/>{{ item.label }}</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, h, resolveDirective, withDirectives } from 'vue';
export default defineComponent({
props: {
items: {
type: Array,
required: true,
},
value: {
required: true,
},
},
render() {
const options = this.$slots.default();
return withDirectives(h('div', {
class: 'pxhvhrfw',
}, options.map(option => h('button', {
class: ['_button', { active: this.value === option.props.value }],
key: option.props.value,
disabled: this.value === option.props.value,
onClick: () => {
this.$emit('update:value', option.props.value);
}
}, option.children))), [
[resolveDirective('size'), { max: [500] }]
]);
}
});
</script>
<style lang="scss" scoped>
<style lang="scss">
.pxhvhrfw {
display: flex;

View File

@@ -9,7 +9,10 @@
<template #header>Req Viewer</template>
<div class="rlkneywz">
<MkTab v-model:value="tab" :items="[{ label: 'Request', value: 'req', }, { label: 'Response', value: 'res', }]" style="border-bottom: solid 1px var(--divider);"/>
<MkTab v-model:value="tab" style="border-bottom: solid 1px var(--divider);">
<option value="req">Request</option>
<option value="res">Response</option>
</MkTab>
<code v-if="tab === 'req'">{{ reqStr }}</code>
<code v-if="tab === 'res'">{{ resStr }}</code>

View File

@@ -4,7 +4,12 @@
<Fa :icon="faTerminal" style="margin-right: 0.5em;"/>Task Manager
</template>
<div class="qljqmnzj">
<MkTab v-model:value="tab" :items="[{ label: 'Windows', value: 'windows', }, { label: 'Stream', value: 'stream', }, { label: 'Stream (Pool)', value: 'streamPool', }, { label: 'API', value: 'api', }]" style="border-bottom: solid 1px var(--divider);"/>
<MkTab v-model:value="tab" style="border-bottom: solid 1px var(--divider);">
<option value="windows">Windows</option>
<option value="stream">Stream</option>
<option value="streamPool">Stream (Pool)</option>
<option value="api">API</option>
</MkTab>
<div class="content">
<div v-if="tab === 'windows'" class="windows" v-follow>

View File

@@ -40,7 +40,7 @@
<section class="_section">
<div class="_content">
<div class="_card">
<div class="_title"><Mfm text="<motion>❤</motion>"/> {{ $t('patrons') }}</div>
<div class="_title"><Mfm text="[jelly ❤]"/> {{ $t('patrons') }}</div>
<div class="_content">
<ul style="margin: 0;">
<li>Gargron</li>

View File

@@ -20,7 +20,7 @@
</div>
</div>
<XPostForm :channel="channel" class="post-form _content _panel _vMargin" fixed/>
<XPostForm :channel="channel" class="post-form _content _panel _vMargin" fixed v-if="this.$store.getters.isSignedIn"/>
<XTimeline class="_content _vMargin" src="channel" :channel="channelId" @before="before" @after="after"/>
</div>

View File

@@ -1,26 +1,30 @@
<template>
<div>
<div class="_section" style="padding: 0;">
<MkTab class="_content" v-model:value="tab" :items="[{ label: $t('_channel.featured'), value: 'featured', icon: faFireAlt }, { label: $t('_channel.following'), value: 'following', icon: faHeart }, { label: $t('_channel.owned'), value: 'owned', icon: faEdit }]"/>
<div class="_section" style="padding: 0;" v-if="this.$store.getters.isSignedIn">
<MkTab class="_content" v-model:value="tab">
<option value="featured"><Fa :icon="faFireAlt"/> {{ $t('_channel.featured') }}</option>
<option value="following"><Fa :icon="faHeart"/> {{ $t('_channel.following') }}</option>
<option value="owned"><Fa :icon="faEdit"/> {{ $t('_channel.owned') }}</option>
</MkTab>
</div>
<div class="_section">
<div class="_content grwlizim featured" v-if="tab === 'featured'">
<MkPagination :pagination="featuredPagination" #default="{items}">
<MkChannelPreview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
<MkChannelPreview v-for="channel in items" class="_vMargin" :channel="channel" :key="channel.id"/>
</MkPagination>
</div>
<div class="_content grwlizim following" v-if="tab === 'following'">
<MkPagination :pagination="followingPagination" #default="{items}">
<MkChannelPreview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
<MkChannelPreview v-for="channel in items" class="_vMargin" :channel="channel" :key="channel.id"/>
</MkPagination>
</div>
<div class="_content grwlizim owned" v-if="tab === 'owned'">
<MkButton class="new" @click="create()"><Fa :icon="faPlus"/></MkButton>
<MkPagination :pagination="ownedPagination" #default="{items}">
<MkChannelPreview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
<MkChannelPreview v-for="channel in items" class="_vMargin" :channel="channel" :key="channel.id"/>
</MkPagination>
</div>
</div>
@@ -44,7 +48,11 @@ export default defineComponent({
return {
INFO: {
title: this.$t('channel'),
icon: faSatelliteDish
icon: faSatelliteDish,
action: {
icon: faPlus,
handler: this.create
}
},
tab: 'featured',
featuredPagination: {
@@ -69,23 +77,3 @@ export default defineComponent({
}
});
</script>
<style lang="scss" scoped>
.grwlizim {
padding: 16px 0;
&.my .uveselbe:first-child {
margin-top: 16px;
}
.uveselbe:not(:last-child) {
margin-bottom: 8px;
}
@media (min-width: 500px) {
.uveselbe:not(:last-child) {
margin-bottom: 16px;
}
}
}
</style>

154
src/client/pages/clip.vue Normal file
View File

@@ -0,0 +1,154 @@
<template>
<div v-if="clip" class="_section">
<div class="okzinsic _content _panel _vMargin">
<div class="description" v-if="clip.description">
<Mfm :text="clip.description" :is-note="false" :i="$store.state.i"/>
</div>
<div class="user">
<MkAvatar :user="clip.user" class="avatar"/> <MkUserName :user="clip.user" :nowrap="false"/>
</div>
</div>
<XNotes class="_content _vMargin" :pagination="pagination" :detail="true"/>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { faEllipsisH, faPaperclip, faPencilAlt, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import MkContainer from '@/components/ui/container.vue';
import XPostForm from '@/components/post-form.vue';
import XNotes from '@/components/notes.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkContainer,
XPostForm,
XNotes,
},
props: {
clipId: {
type: String,
required: true
}
},
data() {
return {
INFO: computed(() => this.clip ? {
title: this.clip.name,
icon: faPaperclip,
action: {
icon: faEllipsisH,
handler: this.menu
}
} : null),
clip: null,
pagination: {
endpoint: 'clips/notes',
limit: 10,
params: () => ({
clipId: this.clipId,
})
},
};
},
computed: {
isOwned(): boolean {
return this.$store.getters.isSignedIn && this.clip && (this.$store.state.i.id === this.clip.userId);
}
},
watch: {
clipId: {
async handler() {
this.clip = await os.api('clips/show', {
clipId: this.clipId,
});
},
immediate: true
}
},
created() {
},
methods: {
menu(ev) {
os.modalMenu([this.isOwned ? {
icon: faPencilAlt,
text: this.$t('edit'),
action: async () => {
const { canceled, result } = await os.form(this.clip.name, {
name: {
type: 'string',
label: this.$t('name'),
default: this.clip.name
},
description: {
type: 'string',
required: false,
multiline: true,
label: this.$t('description'),
default: this.clip.description
},
isPublic: {
type: 'boolean',
label: this.$t('public'),
default: this.clip.isPublic
}
});
if (canceled) return;
os.apiWithDialog('clips/update', {
clipId: this.clip.id,
...result
});
}
} : undefined, this.isOwned ? {
icon: faTrashAlt,
text: this.$t('delete'),
danger: true,
action: async () => {
const { canceled } = await os.dialog({
type: 'warning',
text: this.$t('deleteAreYouSure', { x: this.clip.name }),
showCancelButton: true
});
if (canceled) return;
await os.apiWithDialog('clips/delete', {
clipId: this.clip.id,
});
}
} : undefined], ev.currentTarget || ev.target);
}
}
});
</script>
<style lang="scss" scoped>
.okzinsic {
position: relative;
> .description {
padding: 16px;
}
> .user {
$height: 32px;
padding: 16px;
border-top: solid 1px var(--divider);
line-height: $height;
> .avatar {
width: $height;
height: $height;
}
}
}
</style>

View File

@@ -1,7 +1,10 @@
<template>
<div class="mk-instance-emojis">
<div class="_section" style="padding: 0;">
<MkTab v-model:value="tab" :items="[{ label: $t('local'), value: 'local' }, { label: $t('remote'), value: 'remote' }]"/>
<MkTab v-model:value="tab">
<option value="local">{{ $t('local') }}</option>
<option value="remote">{{ $t('remote') }}</option>
</MkTab>
</div>
<div class="_section">

View File

@@ -1,6 +1,6 @@
<template>
<div v-if="meta">
<section class="_section info">
<div v-if="meta" class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
<div class="_content">
<MkInput v-model:value="name">{{ $t('instanceName') }}</MkInput>
@@ -16,7 +16,7 @@
</div>
</section>
<section class="_section info">
<section class="_card _vMargin">
<div class="_content">
<MkInput v-model:value="maxNoteTextLength" type="number" :save="() => save()"><template #icon><Fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</MkInput>
</div>
@@ -30,7 +30,7 @@
</div>
</section>
<section class="_section info">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faUser"/> {{ $t('registration') }}</div>
<div class="_content">
<MkSwitch v-model:value="enableRegistration" @update:value="save()">{{ $t('enableRegistration') }}</MkSwitch>
@@ -38,7 +38,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faShieldAlt"/> {{ $t('hcaptcha') }}</div>
<div class="_content">
<MkSwitch v-model:value="enableHcaptcha">{{ $t('enableHcaptcha') }}</MkSwitch>
@@ -56,7 +56,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
<div class="_content">
<MkSwitch v-model:value="enableRecaptcha" ref="enableRecaptcha">{{ $t('enableRecaptcha') }}</MkSwitch>
@@ -74,7 +74,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faEnvelope" /> {{ $t('emailConfig') }}</div>
<div class="_content">
<MkSwitch v-model:value="enableEmail" @update:value="save()">{{ $t('enableEmail') }}<template #desc>{{ $t('emailConfigInfo') }}</template></MkSwitch>
@@ -97,7 +97,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
<div class="_content">
<MkSwitch v-model:value="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></MkSwitch>
@@ -113,7 +113,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
<div class="_content">
<MkTextarea v-model:value="pinnedUsers">
@@ -125,7 +125,19 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faThumbtack"/> {{ $t('pinnedPages') }}</div>
<div class="_content">
<MkTextarea v-model:value="pinnedPages">
<template #desc>{{ $t('pinnedPagesDescription') }}</template>
</MkTextarea>
</div>
<div class="_footer">
<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</section>
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faCloud"/> {{ $t('files') }}</div>
<div class="_content">
<MkSwitch v-model:value="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></MkSwitch>
@@ -138,7 +150,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faCloud"/> {{ $t('objectStorage') }}</div>
<div class="_content">
<MkSwitch v-model:value="useObjectStorage">{{ $t('useObjectStorage') }}</MkSwitch>
@@ -166,7 +178,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
<div class="_content">
<MkInput :value="proxyAccount ? proxyAccount.username : null" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></MkInput>
@@ -174,7 +186,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
<div class="_content">
<MkTextarea v-model:value="blockedHosts">
@@ -186,7 +198,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
<div class="_content">
<header><Fa :icon="faTwitter"/> Twitter</header>
@@ -220,7 +232,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faArchway" /> Summaly Proxy</div>
<div class="_content">
<MkInput v-model:value="summalyProxy">URL</MkInput>
@@ -260,6 +272,7 @@ export default defineComponent({
title: this.$t('instance'),
icon: faCog,
},
meta: null,
url,
proxyAccount: null,
proxyAccountId: null,
@@ -269,6 +282,7 @@ export default defineComponent({
remoteDriveCapacityMb: 0,
blockedHosts: '',
pinnedUsers: '',
pinnedPages: '',
maintainerName: null,
maintainerEmail: null,
name: null,
@@ -323,13 +337,9 @@ export default defineComponent({
}
},
computed: {
meta() {
return this.$store.state.instance.meta;
},
},
async created() {
this.meta = await os.api('meta', { detail: true });
created() {
this.name = this.meta.name;
this.description = this.meta.description;
this.tosUrl = this.meta.tosUrl;
@@ -356,6 +366,7 @@ export default defineComponent({
this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
this.blockedHosts = this.meta.blockedHosts.join('\n');
this.pinnedUsers = this.meta.pinnedUsers.join('\n');
this.pinnedPages = this.meta.pinnedPages.join('\n');
this.enableServiceWorker = this.meta.enableServiceWorker;
this.swPublicKey = this.meta.swPublickey;
this.swPrivateKey = this.meta.swPrivateKey;
@@ -506,6 +517,7 @@ export default defineComponent({
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
blockedHosts: this.blockedHosts.split('\n') || [],
pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [],
pinnedPages: this.pinnedPages ? this.pinnedPages.split('\n') : [],
enableServiceWorker: this.enableServiceWorker,
swPublicKey: this.swPublicKey,
swPrivateKey: this.swPrivateKey,

View File

@@ -6,7 +6,7 @@
<XAntenna v-if="draft" :antenna="draft" @created="onAntennaCreated" style="margin-bottom: var(--margin);"/>
<MkPagination :pagination="pagination" #default="{items}" class="antennas" ref="list">
<XAntenna v-for="(antenna, i) in items" :key="antenna.id" :antenna="antenna" @created="onAntennaDeleted"/>
<XAntenna v-for="(antenna, i) in items" :key="antenna.id" :antenna="antenna" @deleted="onAntennaDeleted"/>
</MkPagination>
</div>
</div>

View File

@@ -0,0 +1,105 @@
<template>
<div class="_section qtcaoidl">
<MkButton @click="create" primary class="add"><Fa :icon="faPlus"/> {{ $t('add') }}</MkButton>
<div class="_content">
<MkPagination :pagination="pagination" #default="{items}" ref="list" class="list">
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _vMargin">
<b>{{ item.name }}</b>
<div v-if="item.description" class="description">{{ item.description }}</div>
</MkA>
</MkPagination>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPlus, faPaperclip } from '@fortawesome/free-solid-svg-icons';
import MkPagination from '@/components/ui/pagination.vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkPagination,
MkButton,
},
data() {
return {
INFO: {
title: this.$t('clip'),
icon: faPaperclip,
action: {
icon: faPlus,
handler: this.create
}
},
pagination: {
endpoint: 'clips/list',
limit: 10,
},
draft: null,
faPlus
};
},
methods: {
async create() {
const { canceled, result } = await os.form(this.$t('createNewClip'), {
name: {
type: 'string',
label: this.$t('name')
},
description: {
type: 'string',
required: false,
multiline: true,
label: this.$t('description')
},
isPublic: {
type: 'boolean',
label: this.$t('public'),
default: false
}
});
if (canceled) return;
os.apiWithDialog('clips/create', result);
},
onClipCreated() {
this.$refs.list.reload();
this.draft = null;
},
onClipDeleted() {
this.$refs.list.reload();
},
}
});
</script>
<style lang="scss" scoped>
.qtcaoidl {
> .add {
margin: 0 auto 16px auto;
}
> ._content {
> .list {
> .item {
display: block;
padding: 16px;
> .description {
margin-top: 8px;
padding-top: 8px;
border-top: solid 1px var(--divider);
}
}
}
}
}
</style>

View File

@@ -1,7 +1,11 @@
<template>
<div class="">
<div class="_section" style="padding: 0;">
<MkTab v-model:value="tab" :items="[{ label: $t('ownedGroups'), value: 'owned' }, { label: $t('joinedGroups'), value: 'joined' }, { label: $t('invites'), icon: faEnvelopeOpenText, value: 'invites' }]"/>
<MkTab v-model:value="tab">
<option value="owned">{{ $t('ownedGroups') }}</option>
<option value="joined">{{ $t('joinedGroups') }}</option>
<option value="invites"><Fa :icon="faEnvelopeOpenText"/> {{ $t('invites') }}</option>
</MkTab>
</div>
<div class="_section">

View File

@@ -1,21 +1,31 @@
<template>
<div class="fcuexfpr">
<div v-if="note" class="note">
<div class="_section">
<XNotes v-if="showNext" class="_content" :pagination="next"/>
<MkButton v-else-if="hasNext" class="load _content" @click="showNext = true"><Fa :icon="faChevronUp"/></MkButton>
<div class="_section" v-if="showNext">
<XNotes class="_content" :pagination="next"/>
</div>
<div class="_section">
<div class="_content">
<div class="_section main">
<MkButton v-if="!showNext && hasNext" class="load next _content" @click="showNext = true"><Fa :icon="faChevronUp"/></MkButton>
<div class="_content _vMargin">
<MkRemoteCaution v-if="note.user.host != null" :href="note.url || note.uri" class="_vMargin"/>
<XNote v-model:note="note" :key="note.id" :detail="true" class="_vMargin"/>
</div>
<div class="_content clips _vMargin" v-if="clips && clips.length > 0">
<div class="title">{{ $t('clip') }}</div>
<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _vMargin">
<b>{{ item.name }}</b>
<div v-if="item.description" class="description">{{ item.description }}</div>
<div class="user">
<MkAvatar :user="item.user" class="avatar"/> <MkUserName :user="item.user" :nowrap="false"/>
</div>
</MkA>
</div>
<MkButton v-if="!showPrev && hasPrev" class="load prev _content" @click="showPrev = true"><Fa :icon="faChevronDown"/></MkButton>
</div>
<div class="_section">
<XNotes v-if="showPrev" class="_content" :pagination="prev"/>
<MkButton v-else-if="hasPrev" class="load _content" @click="showPrev = true"><Fa :icon="faChevronDown"/></MkButton>
<div class="_section" v-if="showPrev">
<XNotes class="_content" :pagination="prev"/>
</div>
</div>
@@ -28,7 +38,6 @@
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { faChevronUp, faChevronDown } from '@fortawesome/free-solid-svg-icons';
import Progress from '@/scripts/loading';
import XNote from '@/components/note.vue';
import XNotes from '@/components/notes.vue';
import MkRemoteCaution from '@/components/remote-caution.vue';
@@ -55,6 +64,7 @@ export default defineComponent({
avatar: this.note.user,
} : null),
note: null,
clips: null,
hasPrev: false,
hasNext: false,
showPrev: false,
@@ -88,11 +98,13 @@ export default defineComponent({
},
methods: {
fetch() {
Progress.start();
os.api('notes/show', {
noteId: this.noteId
}).then(note => {
Promise.all([
os.api('notes/clips', {
noteId: note.id,
}),
os.api('users/notes', {
userId: note.userId,
untilId: note.id,
@@ -103,15 +115,14 @@ export default defineComponent({
sinceId: note.id,
limit: 1,
}),
]).then(([prev, next]) => {
]).then(([clips, prev, next]) => {
this.clips = clips;
this.hasPrev = prev.length !== 0;
this.hasNext = next.length !== 0;
this.note = note;
});
}).catch(e => {
this.error = e;
}).finally(() => {
Progress.done();
});
}
}
@@ -121,10 +132,46 @@ export default defineComponent({
<style lang="scss" scoped>
.fcuexfpr {
> .note {
> ._section {
> .main {
> .load {
min-width: 0;
border-radius: 999px;
&.next {
margin-bottom: var(--margin);
}
&.prev {
margin-top: var(--margin);
}
}
> .clips {
> .title {
font-weight: bold;
padding: 12px;
}
> .item {
display: block;
padding: 16px;
> .description {
padding: 8px 0;
}
> .user {
$height: 32px;
padding-top: 16px;
border-top: solid 1px var(--divider);
line-height: $height;
> .avatar {
width: $height;
height: $height;
}
}
}
}
}
}

View File

@@ -0,0 +1,65 @@
<template>
<XContainer @remove="() => $emit('remove')" :draggable="true">
<template #header><Fa :icon="faStickyNote"/> {{ $t('_pages.blocks.note') }}</template>
<section style="padding: 0 16px 0 16px;">
<MkInput v-model:value="id">
<span>{{ $t('_pages.blocks._note.id') }}</span>
<template #desc>{{ $t('_pages.blocks._note.idDescription') }}</template>
</MkInput>
<MkSwitch v-model:value="value.detailed"><span>{{ $t('_pages.blocks._note.detailed') }}</span></MkSwitch>
<XNote v-if="note" v-model:note="note" :key="note.id + ':' + (value.detailed ? 'detailed' : 'normal')" :detail="value.detailed" style="margin-bottom: 16px;"/>
</section>
</XContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faStickyNote } from '@fortawesome/free-solid-svg-icons';
import XContainer from '../page-editor.container.vue';
import MkInput from '@/components/ui/input.vue';
import MkSwitch from '@/components/ui/switch.vue';
import XNote from '@/components/note.vue';
import * as os from '@/os';
export default defineComponent({
components: {
XContainer, MkInput, MkSwitch, XNote
},
props: {
value: {
required: true
},
},
data() {
return {
id: this.value.note,
note: null,
faStickyNote
};
},
watch: {
id: {
async handler() {
if (this.id && (this.id.startsWith('http://') || this.id.startsWith('https://'))) {
this.value.note = this.id.endsWith('/') ? this.id.substr(0, this.id.length - 1).split('/').pop() : this.id.split('/').pop();
} else {
this.value.note = this.id;
}
this.note = await os.api('notes/show', { noteId: this.value.note });
},
immediate: true
},
},
created() {
if (this.value.note == null) this.value.note = null;
if (this.value.detailed == null) this.value.detailed = false;
},
});
</script>

View File

@@ -20,12 +20,13 @@ import XPost from './els/page-editor.el.post.vue';
import XCounter from './els/page-editor.el.counter.vue';
import XRadioButton from './els/page-editor.el.radio-button.vue';
import XCanvas from './els/page-editor.el.canvas.vue';
import XNote from './els/page-editor.el.note.vue';
import * as os from '@/os';
export default defineComponent({
components: {
XDraggable: defineAsyncComponent(() => import('vue-draggable-next').then(x => x.VueDraggableNext)),
XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost, XCounter, XRadioButton, XCanvas
XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost, XCounter, XRadioButton, XCanvas, XNote
},
props: {

View File

@@ -1,57 +1,54 @@
<template>
<div class="_section">
<div class="_content">
<div class="gwbmwxkm _panel _vMargin">
<header>
<div class="title"><Fa :icon="faStickyNote"/> {{ readonly ? $t('_pages.readPage') : pageId ? $t('_pages.editPage') : $t('_pages.newPage') }}</div>
<div class="buttons">
<button class="_button" @click="del()" v-if="!readonly"><Fa :icon="faTrashAlt"/></button>
<button class="_button" @click="() => showOptions = !showOptions"><Fa :icon="faCog"/></button>
<button class="_button" @click="save()" v-if="!readonly"><Fa :icon="faSave"/></button>
</div>
</header>
<MkA class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><Fa :icon="faExternalLinkSquareAlt"/> {{ $t('_pages.viewPage') }}</MkA>
<section>
<MkA class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><Fa :icon="faExternalLinkSquareAlt"/> {{ $t('_pages.viewPage') }}</MkA>
<MkButton @click="save" primary class="save" style="margin: 16px auto 16px auto;"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
<MkContainer :body-togglable="true" :expanded="true" class="_vMargin">
<template #header><Fa :icon="faCog"/> {{ $t('_pages.pageSetting') }}</template>
<div class="_section">
<MkInput v-model:value="title">
<span>{{ $t('_pages.title') }}</span>
</MkInput>
<template v-if="showOptions">
<MkInput v-model:value="summary">
<span>{{ $t('_pages.summary') }}</span>
</MkInput>
<MkInput v-model:value="summary">
<span>{{ $t('_pages.summary') }}</span>
</MkInput>
<MkInput v-model:value="name">
<template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
<span>{{ $t('_pages.url') }}</span>
</MkInput>
<MkInput v-model:value="name">
<template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
<span>{{ $t('_pages.url') }}</span>
</MkInput>
<MkSwitch v-model:value="alignCenter">{{ $t('_pages.alignCenter') }}</MkSwitch>
<MkSwitch v-model:value="alignCenter">{{ $t('_pages.alignCenter') }}</MkSwitch>
<MkSelect v-model:value="font">
<template #label>{{ $t('_pages.font') }}</template>
<option value="serif">{{ $t('_pages.fontSerif') }}</option>
<option value="sans-serif">{{ $t('_pages.fontSansSerif') }}</option>
</MkSelect>
<MkSelect v-model:value="font">
<template #label>{{ $t('_pages.font') }}</template>
<option value="serif">{{ $t('_pages.fontSerif') }}</option>
<option value="sans-serif">{{ $t('_pages.fontSansSerif') }}</option>
</MkSelect>
<MkSwitch v-model:value="hideTitleWhenPinned">{{ $t('_pages.hideTitleWhenPinned') }}</MkSwitch>
<MkSwitch v-model:value="hideTitleWhenPinned">{{ $t('_pages.hideTitleWhenPinned') }}</MkSwitch>
<div class="eyeCatch">
<MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage()"><Fa :icon="faPlus"/> {{ $t('_pages.eyeCatchingImageSet') }}</MkButton>
<div v-else-if="eyeCatchingImage">
<img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name"/>
<MkButton @click="removeEyeCatchingImage()" v-if="!readonly"><Fa :icon="faTrashAlt"/> {{ $t('_pages.eyeCatchingImageRemove') }}</MkButton>
</div>
<div class="eyeCatch">
<MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage"><Fa :icon="faPlus"/> {{ $t('_pages.eyeCatchingImageSet') }}</MkButton>
<div v-else-if="eyeCatchingImage">
<img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name" style="max-width: 100%;"/>
<MkButton @click="removeEyeCatchingImage()" v-if="!readonly"><Fa :icon="faTrashAlt"/> {{ $t('_pages.eyeCatchingImageRemove') }}</MkButton>
</div>
</template>
</div>
</div>
</MkContainer>
<MkContainer :body-togglable="true" :expanded="true" class="_vMargin">
<template #header><Fa :icon="faStickyNote"/> {{ $t('_pages.contents') }}</template>
<div class="_section">
<XBlocks class="content" v-model:value="content" :hpml="hpml"/>
<MkButton @click="add()" v-if="!readonly"><Fa :icon="faPlus"/></MkButton>
</section>
</div>
</div>
</MkContainer>
<MkContainer :body-togglable="true" class="_vMargin">
<template #header><Fa :icon="faMagic"/> {{ $t('_pages.variables') }}</template>
@@ -85,14 +82,14 @@
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import { defineComponent, defineAsyncComponent, computed } from 'vue';
import 'prismjs';
import { highlight, languages } from 'prismjs/components/prism-core';
import 'prismjs/components/prism-clike';
import 'prismjs/components/prism-javascript';
import 'prismjs/themes/prism-okaidia.css';
import 'vue-prism-editor/dist/prismeditor.min.css';
import { faICursor, faPlus, faMagic, faCog, faCode, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
import { faICursor, faPlus, faMagic, faCog, faCode, faExternalLinkSquareAlt, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { faSave, faStickyNote, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import { v4 as uuid } from 'uuid';
import XVariable from './page-editor.script-block.vue';
@@ -108,6 +105,7 @@ import { HpmlTypeChecker } from '@/scripts/hpml/type-checker';
import { url } from '@/config';
import { collectPageVars } from '@/scripts/collect-page-vars';
import * as os from '@/os';
import { selectFile } from '@/scripts/select-file';
export default defineComponent({
components: {
@@ -132,6 +130,13 @@ export default defineComponent({
data() {
return {
INFO: computed(() => this.initPageId ? {
title: this.$t('_pages.editPage'),
icon: faPencilAlt,
} : {
title: this.$t('_pages.newPage'),
icon: faPencilAlt,
}),
author: this.$store.state.i,
readonly: false,
page: null,
@@ -149,7 +154,6 @@ export default defineComponent({
variables: [],
hpml: null,
script: '',
showOptions: false,
url,
faPlus, faICursor, faSave, faStickyNote, faMagic, faCog, faTrashAlt, faExternalLinkSquareAlt, faCode
};
@@ -273,7 +277,7 @@ export default defineComponent({
type: 'success',
text: this.$t('_pages.created')
});
this.$router.push(`/my/pages/edit/${this.pageId}`);
this.$router.push(`/pages/edit/${this.pageId}`);
}).catch(onError);
}
},
@@ -292,7 +296,7 @@ export default defineComponent({
type: 'success',
text: this.$t('_pages.deleted')
});
this.$router.push(`/my/pages`);
this.$router.push(`/pages`);
});
});
},
@@ -353,6 +357,7 @@ export default defineComponent({
{ value: 'text', text: this.$t('_pages.blocks.text') },
{ value: 'image', text: this.$t('_pages.blocks.image') },
{ value: 'textarea', text: this.$t('_pages.blocks.textarea') },
{ value: 'note', text: this.$t('_pages.blocks.note') },
{ value: 'canvas', text: this.$t('_pages.blocks.canvas') },
]
}, {
@@ -413,8 +418,8 @@ export default defineComponent({
return list;
},
setEyeCatchingImage() {
os.selectDriveFile(false).then(file => {
setEyeCatchingImage(e) {
selectFile(e.currentTarget || e.target, null, false).then(file => {
this.eyeCatchingImageId = file.id;
});
},

View File

@@ -22,7 +22,7 @@
<div class="_content">
<MkA :to="`./${page.name}/view-source`" class="link">{{ $t('_pages.viewSource') }}</MkA>
<template v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId">
<MkA :to="`/my/pages/edit/${page.id}`" class="link">{{ $t('_pages.editThisPage') }}</MkA>
<MkA :to="`/pages/edit/${page.id}`" class="link">{{ $t('_pages.editThisPage') }}</MkA>
<button v-if="$store.state.i.pinnedPageId === page.id" @click="pin(false)" class="link _textButton">{{ $t('unpin') }}</button>
<button v-else @click="pin(true)" class="link _textButton">{{ $t('pin') }}</button>
</template>

View File

@@ -1,8 +1,18 @@
<template>
<div>
<MkTab v-model:value="tab" :items="[{ label: $t('_pages.my'), value: 'my', icon: faEdit }, { label: $t('_pages.liked'), value: 'liked', icon: faHeart }]"/>
<MkTab v-model:value="tab" v-if="this.$store.getters.isSignedIn">
<option value="featured"><Fa :icon="faFireAlt"/> {{ $t('_pages.featured') }}</option>
<option value="my"><Fa :icon="faEdit"/> {{ $t('_pages.my') }}</option>
<option value="liked"><Fa :icon="faHeart"/> {{ $t('_pages.liked') }}</option>
</MkTab>
<div class="_section">
<div class="rknalgpo _content" v-if="tab === 'featured'">
<MkPagination :pagination="featuredPagesPagination" #default="{items}">
<MkPagePreview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
</MkPagination>
</div>
<div class="rknalgpo _content my" v-if="tab === 'my'">
<MkButton class="new" @click="create()"><Fa :icon="faPlus"/></MkButton>
<MkPagination :pagination="myPagesPagination" #default="{items}">
@@ -21,7 +31,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { faPlus, faEdit } from '@fortawesome/free-solid-svg-icons';
import { faPlus, faEdit, faFireAlt } from '@fortawesome/free-solid-svg-icons';
import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons';
import MkPagePreview from '@/components/page-preview.vue';
import MkPagination from '@/components/ui/pagination.vue';
@@ -42,7 +52,11 @@ export default defineComponent({
handler: this.create
}
},
tab: 'my',
tab: 'featured',
featuredPagesPagination: {
endpoint: 'pages/featured',
noPaging: true,
},
myPagesPagination: {
endpoint: 'i/pages',
limit: 5,
@@ -51,12 +65,12 @@ export default defineComponent({
endpoint: 'i/page-likes',
limit: 5,
},
faStickyNote, faPlus, faEdit, faHeart
faStickyNote, faPlus, faEdit, faHeart, faFireAlt
};
},
methods: {
create() {
this.$router.push(`/my/pages/new`);
this.$router.push(`/pages/new`);
}
}
});

View File

@@ -1,6 +1,9 @@
<template>
<section class="rrfwjxfl _section">
<MkTab v-model:value="tab" :items="[{ label: $t('mutedUsers'), value: 'mute' }, { label: $t('blockedUsers'), value: 'block' }]" style="margin-bottom: var(--margin);"/>
<MkTab v-model:value="tab" style="margin-bottom: var(--margin);">
<option value="mute">{{ $t('mutedUsers') }}</option>
<option value="block">{{ $t('blockedUsers') }}</option>
</MkTab>
<div class="_content" v-if="tab === 'mute'">
<MkPagination :pagination="mutingPagination" class="muting">
<template #empty><MkInfo>{{ $t('noUsers') }}</MkInfo></template>

View File

@@ -3,6 +3,7 @@
<div class="_card">
<div class="_title"><Fa :icon="faLaugh"/> {{ $t('reaction') }}</div>
<div class="_content">
<div class="_caption" style="padding: 0 8px 8px 8px;">{{ $t('reactionSettingDescription') }}</div>
<XDraggable class="zoaiodol" :list="reactions" animation="150" delay="100" delay-on-touch-only="true">
<button class="_button item" v-for="reaction in reactions" :key="reaction" @click="remove(reaction, $event)">
<MkEmoji :emoji="reaction" :normal="true"/>
@@ -11,7 +12,7 @@
<button>a</button>
</template>
</XDraggable>
<div class="_caption" style="padding: 8px;">{{ $t('reactionSettingDescription') }} <button class="_textButton" @click="chooseEmoji">{{ $t('chooseEmoji') }}</button></div>
<div class="_caption" style="padding: 8px;">{{ $t('reactionSettingDescription2') }} <button class="_textButton" @click="chooseEmoji">{{ $t('chooseEmoji') }}</button></div>
<MkSwitch v-model:value="useFullReactionPicker">{{ $t('useFullReactionPicker') }}</MkSwitch>
</div>
<div class="_footer">

View File

@@ -1,7 +1,10 @@
<template>
<div class="_section">
<div class="_card">
<MkTab v-model:value="tab" :items="[{ label: $t('_wordMute.soft'), value: 'soft' }, { label: $t('_wordMute.hard'), value: 'hard' }]"/>
<MkTab v-model:value="tab">
<option value="soft">{{ $t('_wordMute.soft') }}</option>
<option value="hard">{{ $t('_wordMute.hard') }}</option>
</MkTab>
<div class="_content">
<div v-show="tab === 'soft'">
<MkInfo>{{ $t('_wordMute.softDescription') }}</MkInfo>

View File

@@ -162,7 +162,7 @@ export default defineComponent({
dialogCancelByBgClick: true,
dialogInput: false,
dialogResult: null,
formTitle: null,
formTitle: 'Test form',
formForm: JSON.stringify({
foo: {
type: 'boolean',
@@ -179,6 +179,12 @@ export default defineComponent({
default: 'Misskey makes you happy.',
label: 'This is a string property'
},
qux: {
type: 'string',
multiline: true,
default: 'Misskey makes\nyou happy.',
label: 'Multiline string'
},
}, null, '\t'),
formResult: null,
mfm: '',

View File

@@ -0,0 +1,141 @@
<template>
<div class="xyeqzsjl _panel">
<header>
<button class="_button" @click="back()" v-if="history.length > 0"><Fa :icon="faChevronLeft"/></button>
<XHeader class="title" :info="pageInfo" :with-back="false"/>
</header>
<div>
<component :is="component" v-bind="props" :ref="changePage"/>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
import XWindow from '@/components/ui/window.vue';
import XHeader from '@/ui/_common_/header.vue';
import { popout } from '@/scripts/popout';
import { resolve } from '@/router';
import { url } from '@/config';
export default defineComponent({
components: {
XWindow,
XHeader,
},
provide() {
return {
navHook: (path) => {
this.navigate(path);
}
};
},
props: {
initialPath: {
type: String,
required: true,
},
},
data() {
return {
pageInfo: null,
path: this.initialPath,
component: null,
props: null,
history: [],
faChevronLeft,
};
},
computed: {
url(): string {
return url + this.path;
},
},
created() {
const { component, props } = resolve(this.initialPath);
this.component = component;
this.props = props;
},
methods: {
changePage(page) {
if (page == null) return;
if (page.INFO) {
this.pageInfo = page.INFO;
}
},
navigate(path, record = true) {
if (record) this.history.push(this.path);
this.path = path;
const { component, props } = resolve(path);
this.component = component;
this.props = props;
},
back() {
this.navigate(this.history.pop(), false);
},
expand() {
this.$router.push(this.path);
this.$refs.window.close();
},
popout() {
popout(this.path, this.$el);
this.$refs.window.close();
},
},
});
</script>
<style lang="scss" scoped>
.xyeqzsjl {
--section-padding: 16px;
display: flex;
flex-direction: column;
contain: content;
> header {
$height: 50px;
display: flex;
position: relative;
z-index: 1;
height: $height;
line-height: $height;
box-shadow: 0px 1px var(--divider);
> button {
height: $height;
width: $height;
&:hover {
color: var(--fgHighlighted);
}
}
> .title {
flex: 1;
position: relative;
line-height: $height;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
}
> div {
flex: 1;
overflow: auto;
}
}
</style>

View File

@@ -1,18 +1,13 @@
<template>
<div class="rsqzvsbo">
<div class="_section">
<div class="_content _panel about" v-if="meta">
<div class="body">
<div class="desc" v-html="meta.description || $t('introMisskey')"></div>
<MkButton @click="signup()" style="display: inline-block; margin-right: 16px;" primary>{{ $t('signup') }}</MkButton>
<MkButton @click="signin()" style="display: inline-block;">{{ $t('login') }}</MkButton>
</div>
</div>
<div class="rsqzvsbo _section" v-if="meta">
<div class="about">
<h1>{{ instanceName }}</h1>
<div class="desc" v-html="meta.description || $t('introMisskey')"></div>
<MkButton @click="signup()" style="display: inline-block; margin-right: 16px;" primary>{{ $t('signup') }}</MkButton>
<MkButton @click="signin()" style="display: inline-block;">{{ $t('login') }}</MkButton>
</div>
<div class="_section">
<div class="_content">
<XNotes :pagination="featuredPagination"/>
</div>
<div class="blocks">
<XBlock class="block" v-for="path in meta.pinnedPages" :initial-path="path" :key="path"/>
</div>
</div>
</template>
@@ -24,33 +19,30 @@ import XSigninDialog from '@/components/signin-dialog.vue';
import XSignupDialog from '@/components/signup-dialog.vue';
import MkButton from '@/components/ui/button.vue';
import XNotes from '@/components/notes.vue';
import { host } from '@/config';
import XBlock from './welcome.entrance.block.vue';
import { host, instanceName } from '@/config';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
XNotes,
XBlock,
},
data() {
return {
featuredPagination: {
endpoint: 'notes/featured',
limit: 10,
noPaging: true,
},
host: toUnicode(host),
instanceName,
meta: null,
};
},
computed: {
meta() {
return this.$store.state.instance.meta;
},
},
created() {
os.api('meta', { detail: true }).then(meta => {
this.meta = meta;
});
os.api('stats').then(stats => {
this.stats = stats;
});
@@ -74,15 +66,42 @@ export default defineComponent({
<style lang="scss" scoped>
.rsqzvsbo {
> ._section {
> .about {
> .body {
padding: 32px;
text-align: center;
@media (max-width: 500px) {
padding: 16px;
}
}
> .about {
display: inline-block;
padding: 24px;
margin-bottom: var(--margin);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
background: rgba(0, 0, 0, 0.5);
border-radius: var(--radius);
text-align: center;
box-sizing: border-box;
min-width: 300px;
max-width: 800px;
&, * {
color: #fff !important;
}
> h1 {
margin: 0 0 16px 0;
}
}
> .blocks {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
grid-gap: var(--margin);
text-align: left;
> .block {
height: 600px;
}
@media (max-width: 800px) {
grid-template-columns: 1fr;
}
}
}

View File

@@ -10,6 +10,7 @@ import { defineComponent } from 'vue';
import XSetup from './welcome.setup.vue';
import XEntrance from './welcome.entrance.vue';
import { instanceName } from '@/config';
import * as os from '@/os';
export default defineComponent({
components: {
@@ -20,16 +21,17 @@ export default defineComponent({
data() {
return {
INFO: {
title: instanceName || 'Misskey',
title: instanceName,
icon: null
},
meta: null
}
},
computed: {
meta() {
return this.$store.state.instance.meta;
},
},
created() {
os.api('meta', { detail: true }).then(meta => {
this.meta = meta;
});
}
});
</script>

View File

@@ -33,10 +33,14 @@ export const router = createRouter({
{ path: '/explore', component: page('explore') },
{ path: '/explore/tags/:tag', props: true, component: page('explore') },
{ path: '/search', component: page('search') },
{ path: '/pages', name: 'pages', component: page('pages') },
{ path: '/pages/new', component: page('page-editor/page-editor') },
{ path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
{ path: '/channels', component: page('channels') },
{ path: '/channels/new', component: page('channel-editor') },
{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true },
{ path: '/channels/:channelId', component: page('channel'), props: route => ({ channelId: route.params.channelId }) },
{ path: '/clips/:clipId', component: page('clip'), props: route => ({ clipId: route.params.clipId }) },
{ path: '/my/notifications', component: page('notifications') },
{ path: '/my/favorites', component: page('favorites') },
{ path: '/my/messages', component: page('messages') },
@@ -46,15 +50,13 @@ export const router = createRouter({
{ path: '/my/messaging/group/:group', component: page('messaging/messaging-room'), props: route => ({ groupId: route.params.group }) },
{ path: '/my/drive', name: 'drive', component: page('drive') },
{ path: '/my/drive/folder/:folder', component: page('drive') },
{ path: '/my/pages', name: 'pages', component: page('pages') },
{ path: '/my/pages/new', component: page('page-editor/page-editor') },
{ path: '/my/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
{ path: '/my/follow-requests', component: page('follow-requests') },
{ path: '/my/lists', component: page('my-lists/index') },
{ path: '/my/lists/:list', component: page('my-lists/list') },
{ path: '/my/groups', component: page('my-groups/index') },
{ path: '/my/groups/:group', component: page('my-groups/group') },
{ path: '/my/antennas', component: page('my-antennas/index') },
{ path: '/my/clips', component: page('my-clips/index') },
{ path: '/my/apps', component: page('apps') },
{ path: '/scratchpad', component: page('scratchpad') },
{ path: '/instance', component: page('instance/index') },

View File

@@ -1,5 +1,5 @@
import { faBell, faComments, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import { faAt, faBroadcastTower, faCloud, faColumns, faDoorClosed, faFileAlt, faFireAlt, faGamepad, faHashtag, faListUl, faSatellite, faSatelliteDish, faSearch, faStar, faTerminal, faUserClock, faUsers } from '@fortawesome/free-solid-svg-icons';
import { faAt, faBroadcastTower, faCloud, faColumns, faDoorClosed, faFileAlt, faFireAlt, faGamepad, faHashtag, faListUl, faPaperclip, faSatellite, faSatelliteDish, faSearch, faStar, faTerminal, faUserClock, faUsers } from '@fortawesome/free-solid-svg-icons';
import { computed } from 'vue';
import { store } from '@/store';
import { search } from '@/scripts/search';
@@ -96,8 +96,13 @@ export const sidebarDef = {
pages: {
title: 'pages',
icon: faFileAlt,
to: '/pages',
},
clips: {
title: 'clip',
icon: faPaperclip,
show: computed(() => store.getters.isSignedIn),
to: '/my/pages',
to: '/my/clips',
},
channels: {
title: 'channel',

View File

@@ -7,12 +7,12 @@
<MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName }) }}</MkA>
</header>
<div class="banner" :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
<h1>{{ instanceName }}</h1>
<div class="banner" :class="{ asBg: $route.path === '/' }" :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
<h1 v-if="$route.path !== '/'">{{ instanceName }}</h1>
</div>
<div class="contents" ref="contents" :class="{ wallpaper }">
<header class="header" ref="header">
<header class="header" ref="header" v-show="$route.path !== '/'">
<XHeader :info="pageInfo"/>
</header>
<main ref="main">
@@ -116,11 +116,10 @@ export default defineComponent({
<style lang="scss" scoped>
.mk-app {
min-height: 100vh;
max-width: 1300px;
margin: 0 auto;
box-shadow: 1px 0 var(--divider), -1px 0 var(--divider);
> header {
position: relative;
z-index: 1;
background: var(--panel);
padding: 0 16px;
text-align: center;
@@ -145,6 +144,12 @@ export default defineComponent({
background-size: cover;
background-position: center;
&.asBg {
position: absolute;
left: 0;
height: 320px;
}
&:after {
content: "";
display: block;
@@ -166,6 +171,9 @@ export default defineComponent({
}
> .contents {
position: relative;
z-index: 1;
> .header {
position: sticky;
top: 0;

View File

@@ -35,4 +35,10 @@ export class Clip {
default: false
})
public isPublic: boolean;
@Column('varchar', {
length: 2048, nullable: true, default: null,
comment: 'The description of the Clip.'
})
public description: string | null;
}

View File

@@ -76,6 +76,11 @@ export class Meta {
})
public blockedHosts: string[];
@Column('varchar', {
length: 512, array: true, default: '{"/announcements", "/featured", "/channels", "/explore", "/games/reversi", "/about-misskey"}'
})
public pinnedPages: string[];
@Column('varchar', {
length: 512,
nullable: true,

View File

@@ -2,6 +2,8 @@ import { EntityRepository, Repository } from 'typeorm';
import { Clip } from '../entities/clip';
import { ensure } from '../../prelude/ensure';
import { SchemaType } from '../../misc/schema';
import { Users } from '..';
import { awaitAll } from '../../prelude/await-all';
export type PackedClip = SchemaType<typeof packedClipSchema>;
@@ -12,11 +14,15 @@ export class ClipRepository extends Repository<Clip> {
): Promise<PackedClip> {
const clip = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return {
return await awaitAll({
id: clip.id,
createdAt: clip.createdAt.toISOString(),
userId: clip.userId,
user: Users.pack(clip.user || clip.userId),
name: clip.name,
};
description: clip.description,
isPublic: clip.isPublic,
});
}
}
@@ -37,10 +43,30 @@ export const packedClipSchema = {
format: 'date-time',
description: 'The date that the Clip was created.'
},
userId: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
user: {
type: 'object' as const,
ref: 'User',
optional: false as const, nullable: false as const,
},
name: {
type: 'string' as const,
optional: false as const, nullable: false as const,
description: 'The name of the Clip.'
},
description: {
type: 'string' as const,
optional: false as const, nullable: true as const,
description: 'The description of the Clip.'
},
isPublic: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
description: 'Whether this Clip is public.',
},
},
};

View File

@@ -85,8 +85,9 @@ export class PageRepository extends Repository<Page> {
public packMany(
pages: Page[],
me?: User['id'] | User | null | undefined,
) {
return Promise.all(pages.map(x => this.pack(x)));
return Promise.all(pages.map(x => this.pack(x, me)));
}
}

View File

@@ -208,6 +208,10 @@ export const meta = {
}
},
pinnedPages: {
validator: $.optional.arr($.str),
},
langs: {
validator: $.optional.arr($.str),
desc: {
@@ -537,6 +541,10 @@ export default define(meta, async (ps, me) => {
set.langs = ps.langs.filter(Boolean);
}
if (Array.isArray(ps.pinnedPages)) {
set.pinnedPages = ps.pinnedPages.filter(Boolean);
}
if (ps.summalyProxy !== undefined) {
set.summalyProxy = ps.summalyProxy;
}

View File

@@ -0,0 +1,76 @@
import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { ClipNotes, Clips } from '../../../../models';
import { ApiError } from '../../error';
import { genId } from '../../../../misc/gen-id';
import { getNote } from '../../common/getters';
export const meta = {
tags: ['account', 'notes', 'clips'],
requireCredential: true as const,
kind: 'write:account',
params: {
clipId: {
validator: $.type(ID),
},
noteId: {
validator: $.type(ID),
},
},
errors: {
noSuchClip: {
message: 'No such clip.',
code: 'NO_SUCH_CLIP',
id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf'
},
noSuchNote: {
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b'
},
alreadyClipped: {
message: 'The note has already been clipped.',
code: 'ALREADY_CLIPPED',
id: '734806c4-542c-463a-9311-15c512803965'
},
}
};
export default define(meta, async (ps, user) => {
const clip = await Clips.findOne({
id: ps.clipId,
userId: user.id
});
if (clip == null) {
throw new ApiError(meta.errors.noSuchClip);
}
const note = await getNote(ps.noteId).catch(e => {
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw e;
});
const exist = await ClipNotes.findOne({
noteId: note.id,
clipId: clip.id
});
if (exist != null) {
throw new ApiError(meta.errors.alreadyClipped);
}
await ClipNotes.save({
id: genId(),
noteId: note.id,
clipId: clip.id
});
});

View File

@@ -13,6 +13,14 @@ export const meta = {
params: {
name: {
validator: $.str.range(1, 100)
},
isPublic: {
validator: $.optional.bool
},
description: {
validator: $.optional.nullable.str.range(1, 2048)
}
},
};
@@ -23,6 +31,8 @@ export default define(meta, async (ps, user) => {
createdAt: new Date(),
userId: user.id,
name: ps.name,
isPublic: ps.isPublic,
description: ps.description,
});
return await Clips.pack(clip);

View File

@@ -1,19 +1,24 @@
import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { Clips, Notes } from '../../../../models';
import { ClipNotes, Clips, Notes } from '../../../../models';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
import { ApiError } from '../../error';
export const meta = {
tags: ['account', 'notes', 'clips'],
requireCredential: true as const,
requireCredential: false as const,
kind: 'read:account',
params: {
clipId: {
validator: $.type(ID),
},
limit: {
validator: $.optional.num.range(1, 100),
default: 10
@@ -30,7 +35,7 @@ export const meta = {
errors: {
noSuchClip: {
message: 'No such list.',
message: 'No such clip.',
code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00'
}
@@ -40,13 +45,16 @@ export const meta = {
export default define(meta, async (ps, user) => {
const clip = await Clips.findOne({
id: ps.clipId,
userId: user.id
});
if (clip == null) {
throw new ApiError(meta.errors.noSuchClip);
}
if (!clip.isPublic && (user == null || (clip.userId !== user.id))) {
throw new ApiError(meta.errors.noSuchClip);
}
const clipQuery = ClipNotes.createQueryBuilder('joining')
.select('joining.noteId')
.where('joining.clipId = :clipId', { clipId: clip.id });
@@ -56,8 +64,10 @@ export default define(meta, async (ps, user) => {
.leftJoinAndSelect('note.user', 'user')
.setParameters(clipQuery.getParameters());
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
if (user) {
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
}
const notes = await query
.take(ps.limit!)

View File

@@ -7,7 +7,7 @@ import { Clips } from '../../../../models';
export const meta = {
tags: ['clips', 'account'],
requireCredential: true as const,
requireCredential: false as const,
kind: 'read:account',
@@ -30,12 +30,15 @@ export default define(meta, async (ps, me) => {
// Fetch the clip
const clip = await Clips.findOne({
id: ps.clipId,
userId: me.id,
});
if (clip == null) {
throw new ApiError(meta.errors.noSuchClip);
}
if (!clip.isPublic && (me == null || (clip.userId !== me.id))) {
throw new ApiError(meta.errors.noSuchClip);
}
return await Clips.pack(clip);
});

View File

@@ -18,6 +18,14 @@ export const meta = {
name: {
validator: $.str.range(1, 100),
},
isPublic: {
validator: $.optional.bool
},
description: {
validator: $.optional.nullable.str.range(1, 2048)
}
},
@@ -42,7 +50,9 @@ export default define(meta, async (ps, user) => {
}
await Clips.update(clip.id, {
name: ps.name
name: ps.name,
description: ps.description,
isPublic: ps.isPublic,
});
return await Clips.pack(clip.id);

View File

@@ -99,8 +99,6 @@ export default define(meta, async (ps, me) => {
}
});
const proxyAccount = instance.proxyAccountId ? await Users.pack(instance.proxyAccountId).catch(() => null) : null;
const response: any = {
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
@@ -122,8 +120,6 @@ export default define(meta, async (ps, me) => {
disableGlobalTimeline: instance.disableGlobalTimeline,
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
cacheRemoteFiles: instance.cacheRemoteFiles,
proxyRemoteFiles: instance.proxyRemoteFiles,
enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableRecaptcha: instance.enableRecaptcha,
@@ -135,9 +131,6 @@ export default define(meta, async (ps, me) => {
iconUrl: instance.iconUrl,
maxNoteTextLength: Math.min(instance.maxNoteTextLength, DB_MAX_NOTE_TEXT_LENGTH),
emojis: await Emojis.packMany(emojis),
requireSetup: (await Users.count({
host: null,
})) === 0,
enableEmail: instance.enableEmail,
enableTwitterIntegration: instance.enableTwitterIntegration,
@@ -146,10 +139,20 @@ export default define(meta, async (ps, me) => {
enableServiceWorker: instance.enableServiceWorker,
proxyAccountName: proxyAccount ? proxyAccount.username : null,
...(ps.detail ? {
pinnedPages: instance.pinnedPages,
cacheRemoteFiles: instance.cacheRemoteFiles,
proxyRemoteFiles: instance.proxyRemoteFiles,
requireSetup: (await Users.count({
host: null,
})) === 0,
} : {})
};
if (ps.detail) {
const proxyAccount = instance.proxyAccountId ? await Users.pack(instance.proxyAccountId).catch(() => null) : null;
response.proxyAccountName = proxyAccount ? proxyAccount.username : null;
response.features = {
registration: !instance.disableRegistration,
localTimeLine: !instance.disableLocalTimeline,
@@ -164,42 +167,42 @@ export default define(meta, async (ps, me) => {
serviceWorker: instance.enableServiceWorker,
miauth: true,
};
}
if (me && me.isAdmin) {
response.useStarForReactionFallback = instance.useStarForReactionFallback;
response.pinnedUsers = instance.pinnedUsers;
response.hiddenTags = instance.hiddenTags;
response.blockedHosts = instance.blockedHosts;
response.hcaptchaSecretKey = instance.hcaptchaSecretKey;
response.recaptchaSecretKey = instance.recaptchaSecretKey;
response.proxyAccountId = instance.proxyAccountId;
response.twitterConsumerKey = instance.twitterConsumerKey;
response.twitterConsumerSecret = instance.twitterConsumerSecret;
response.githubClientId = instance.githubClientId;
response.githubClientSecret = instance.githubClientSecret;
response.discordClientId = instance.discordClientId;
response.discordClientSecret = instance.discordClientSecret;
response.summalyProxy = instance.summalyProxy;
response.email = instance.email;
response.smtpSecure = instance.smtpSecure;
response.smtpHost = instance.smtpHost;
response.smtpPort = instance.smtpPort;
response.smtpUser = instance.smtpUser;
response.smtpPass = instance.smtpPass;
response.swPrivateKey = instance.swPrivateKey;
response.useObjectStorage = instance.useObjectStorage;
response.objectStorageBaseUrl = instance.objectStorageBaseUrl;
response.objectStorageBucket = instance.objectStorageBucket;
response.objectStoragePrefix = instance.objectStoragePrefix;
response.objectStorageEndpoint = instance.objectStorageEndpoint;
response.objectStorageRegion = instance.objectStorageRegion;
response.objectStoragePort = instance.objectStoragePort;
response.objectStorageAccessKey = instance.objectStorageAccessKey;
response.objectStorageSecretKey = instance.objectStorageSecretKey;
response.objectStorageUseSSL = instance.objectStorageUseSSL;
response.objectStorageUseProxy = instance.objectStorageUseProxy;
response.objectStorageSetPublicRead = instance.objectStorageSetPublicRead;
if (me && me.isAdmin) {
response.useStarForReactionFallback = instance.useStarForReactionFallback;
response.pinnedUsers = instance.pinnedUsers;
response.hiddenTags = instance.hiddenTags;
response.blockedHosts = instance.blockedHosts;
response.hcaptchaSecretKey = instance.hcaptchaSecretKey;
response.recaptchaSecretKey = instance.recaptchaSecretKey;
response.proxyAccountId = instance.proxyAccountId;
response.twitterConsumerKey = instance.twitterConsumerKey;
response.twitterConsumerSecret = instance.twitterConsumerSecret;
response.githubClientId = instance.githubClientId;
response.githubClientSecret = instance.githubClientSecret;
response.discordClientId = instance.discordClientId;
response.discordClientSecret = instance.discordClientSecret;
response.summalyProxy = instance.summalyProxy;
response.email = instance.email;
response.smtpSecure = instance.smtpSecure;
response.smtpHost = instance.smtpHost;
response.smtpPort = instance.smtpPort;
response.smtpUser = instance.smtpUser;
response.smtpPass = instance.smtpPass;
response.swPrivateKey = instance.swPrivateKey;
response.useObjectStorage = instance.useObjectStorage;
response.objectStorageBaseUrl = instance.objectStorageBaseUrl;
response.objectStorageBucket = instance.objectStorageBucket;
response.objectStoragePrefix = instance.objectStoragePrefix;
response.objectStorageEndpoint = instance.objectStorageEndpoint;
response.objectStorageRegion = instance.objectStorageRegion;
response.objectStoragePort = instance.objectStoragePort;
response.objectStorageAccessKey = instance.objectStorageAccessKey;
response.objectStorageSecretKey = instance.objectStorageSecretKey;
response.objectStorageUseSSL = instance.objectStorageUseSSL;
response.objectStorageUseProxy = instance.objectStorageUseProxy;
response.objectStorageSetPublicRead = instance.objectStorageSetPublicRead;
}
}
return response;

View File

@@ -0,0 +1,54 @@
import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { ClipNotes, Clips } from '../../../../models';
import { getNote } from '../../common/getters';
import { ApiError } from '../../error';
import { In } from 'typeorm';
export const meta = {
tags: ['clips', 'notes'],
requireCredential: false as const,
params: {
noteId: {
validator: $.type(ID),
},
},
res: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'Note',
}
},
errors: {
noSuchNote: {
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: '47db1a1c-b0af-458d-8fb4-986e4efafe1e'
}
}
};
export default define(meta, async (ps, me) => {
const note = await getNote(ps.noteId).catch(e => {
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw e;
});
const clipNotes = await ClipNotes.find({
noteId: note.id,
});
const clips = await Clips.find({
id: In(clipNotes.map(x => x.clipId)),
});
return await Promise.all(clips.map(x => Clips.pack(x)));
});

View File

@@ -0,0 +1,29 @@
import define from '../../define';
import { Pages } from '../../../../models';
export const meta = {
tags: ['pages'],
requireCredential: false as const,
res: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'Page',
}
},
};
export default define(meta, async (ps, me) => {
const query = Pages.createQueryBuilder('page')
.where('page.visibility = \'public\'')
.andWhere('page.likedCount > 0')
.orderBy('page.likedCount', 'DESC');
const pages = await query.take(10).getMany();
return await Pages.packMany(pages, me);
});

View File

@@ -17,7 +17,7 @@ import packFeed from './feed';
import { fetchMeta } from '../../misc/fetch-meta';
import { genOpenapiSpec } from '../api/openapi/gen-spec';
import config from '../../config';
import { Users, Notes, Emojis, UserProfiles, Pages, Channels } from '../../models';
import { Users, Notes, Emojis, UserProfiles, Pages, Channels, Clips } from '../../models';
import parseAcct from '../../misc/acct/parse';
import getNoteSummary from '../../misc/get-note-summary';
import { ensure } from '../../prelude/ensure';
@@ -298,6 +298,29 @@ router.get('/@:user/pages/:page', async ctx => {
ctx.status = 404;
});
// Clip
// TODO: 非publicなclipのハンドリング
router.get('/clips/:clip', async ctx => {
const clip = await Clips.findOne({
id: ctx.params.clip,
});
if (clip) {
const _clip = await Clips.pack(clip);
const meta = await fetchMeta();
await ctx.render('clip', {
clip: _clip,
instanceName: meta.name || 'Misskey'
});
ctx.set('Cache-Control', 'public, max-age=180');
return;
}
ctx.status = 404;
});
// Channel
router.get('/channels/:channel', async ctx => {
const channel = await Channels.findOne({

View File

@@ -0,0 +1,30 @@
extends ./base
block vars
- const user = clip.user;
- const title = clip.name;
- const url = `${config.url}/clips/${clip.id}`;
block title
= `${title} | ${instanceName}`
block desc
meta(name='description' content= clip.description)
block og
meta(property='og:type' content='article')
meta(property='og:title' content= title)
meta(property='og:description' content= clip.description)
meta(property='og:url' content= url)
meta(property='og:image' content= user.avatarUrl)
block meta
meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id)
meta(name='misskey:clip-id' content=clip.id)
meta(name='twitter:card' content='summary')
// todo
if user.twitter
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)