mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-26 11:57:34 -07:00
Merge branch 'develop' of codeberg.org:calckey/calckey into develop
This commit is contained in:
commit
e88ab6cb0f
20 changed files with 359 additions and 138 deletions
|
@ -1114,6 +1114,9 @@ isPatron: "Calckey Patron"
|
||||||
reactionPickerSkinTone: "Preferred emoji skin tone"
|
reactionPickerSkinTone: "Preferred emoji skin tone"
|
||||||
enableServerMachineStats: "Enable server hardware statistics"
|
enableServerMachineStats: "Enable server hardware statistics"
|
||||||
enableIdenticonGeneration: "Enable Identicon generation"
|
enableIdenticonGeneration: "Enable Identicon generation"
|
||||||
|
showPopup: "Notify users with popup"
|
||||||
|
showWithSparkles: "Show with sparkles"
|
||||||
|
youHaveUnreadAnnouncements: "You have unread announcements"
|
||||||
|
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "Reduces the effort of server moderation through automatically recognizing
|
description: "Reduces the effort of server moderation through automatically recognizing
|
||||||
|
|
|
@ -980,6 +980,9 @@ preventAiLearningDescription: "投稿したノート、添付した画像など
|
||||||
noGraze: "ブラウザの拡張機能「Graze for Mastodon」は、Calckeyの動作を妨げるため、無効にしてください。"
|
noGraze: "ブラウザの拡張機能「Graze for Mastodon」は、Calckeyの動作を妨げるため、無効にしてください。"
|
||||||
enableServerMachineStats: "サーバーのマシン情報を公開する"
|
enableServerMachineStats: "サーバーのマシン情報を公開する"
|
||||||
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
|
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
|
||||||
|
showPopup: "ポップアップを表示してユーザーに知らせる"
|
||||||
|
showWithSparkles: "タイトルをキラキラさせる"
|
||||||
|
youHaveUnreadAnnouncements: "未読のお知らせがあります"
|
||||||
|
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"
|
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
---
|
|
||||||
_lang_: "Türkçe"
|
_lang_: "Türkçe"
|
||||||
introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Calckey'e hoş geldiniz.\nMisskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀."
|
introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Calckey'e hoş geldiniz.\n
|
||||||
|
Misskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\"\
|
||||||
|
\ oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\n
|
||||||
|
Herkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini
|
||||||
|
de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀."
|
||||||
monthAndDay: "{month}Ay {day}Gün"
|
monthAndDay: "{month}Ay {day}Gün"
|
||||||
search: "Arama"
|
search: "Arama"
|
||||||
notifications: "Bildirim"
|
notifications: "Bildirimler"
|
||||||
username: "Kullanıcı Adı"
|
username: "Kullanıcı Adı"
|
||||||
password: "Şifre"
|
password: "Şifre"
|
||||||
forgotPassword: "şifremi unuttum"
|
forgotPassword: "şifremi unuttum"
|
||||||
|
@ -11,7 +14,7 @@ ok: "TAMAM"
|
||||||
gotIt: "Anladım"
|
gotIt: "Anladım"
|
||||||
cancel: "İptal"
|
cancel: "İptal"
|
||||||
enterUsername: "Kullanıcı adınızı giriniz"
|
enterUsername: "Kullanıcı adınızı giriniz"
|
||||||
noNotes: "Notlar mevcut değil."
|
noNotes: "Gönderiler mevcut değil."
|
||||||
noNotifications: "Bildirim bulunmuyor"
|
noNotifications: "Bildirim bulunmuyor"
|
||||||
settings: "Ayarlar"
|
settings: "Ayarlar"
|
||||||
basicSettings: "Temel Ayarlar"
|
basicSettings: "Temel Ayarlar"
|
||||||
|
@ -37,7 +40,8 @@ copyContent: "İçeriği kopyala"
|
||||||
copyLink: "Bağlantıyı Kopyala"
|
copyLink: "Bağlantıyı Kopyala"
|
||||||
delete: "Sil"
|
delete: "Sil"
|
||||||
deleteAndEdit: "Sil ve yeniden düzenle"
|
deleteAndEdit: "Sil ve yeniden düzenle"
|
||||||
deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir."
|
deleteAndEditConfirm: "Bu gönderiyi silip yeniden düzenlemek istiyor musunuz? Bu gönderiye
|
||||||
|
ilişkin tüm tepkiler, destekler ve yanıtlar silinecektir."
|
||||||
addToList: "Listeye ekle"
|
addToList: "Listeye ekle"
|
||||||
sendMessage: "Mesaj Gönder"
|
sendMessage: "Mesaj Gönder"
|
||||||
copyUsername: "Kullanıcı Adını Kopyala"
|
copyUsername: "Kullanıcı Adını Kopyala"
|
||||||
|
@ -61,3 +65,19 @@ _deck:
|
||||||
_columns:
|
_columns:
|
||||||
notifications: "Bildirim"
|
notifications: "Bildirim"
|
||||||
tl: "Zaman çizelgesi"
|
tl: "Zaman çizelgesi"
|
||||||
|
searchPlaceholder: Calckey'de Ara
|
||||||
|
reply: Yanıtla
|
||||||
|
jumpToPrevious: Öncekini görüntüle
|
||||||
|
deleted: Silindi
|
||||||
|
editNote: Notu düzenle
|
||||||
|
noThankYou: Hayır, teşekkürler
|
||||||
|
addInstance: Bir sunucu ekle
|
||||||
|
cantFavorite: Favorilere eklenemedi.
|
||||||
|
edited: '{date} tarihinde ve {time} vaktinde düzenlendi'
|
||||||
|
loggingIn: Giriş Yapılıyor
|
||||||
|
save: Kaydet
|
||||||
|
headlineMisskey: Sonsuza kadar ücretsiz, açık kaynak kodlu, merkeziyetsiz sosyal medya
|
||||||
|
platformu! 🚀
|
||||||
|
loadMore: Daha fazla yükle
|
||||||
|
instance: Sunucu
|
||||||
|
fetchingAsApObject: Fedevren'den çekiliyor
|
||||||
|
|
|
@ -702,7 +702,8 @@ onlineUsersCount: "{n}人正在線上"
|
||||||
nUsers: "{n}用戶"
|
nUsers: "{n}用戶"
|
||||||
nNotes: "{n}貼文"
|
nNotes: "{n}貼文"
|
||||||
sendErrorReports: "傳送錯誤報告"
|
sendErrorReports: "傳送錯誤報告"
|
||||||
sendErrorReportsDescription: "啟用後,問題報告將傳送至Calckey開發者以提升軟體品質。\n問題報告可能包括OS版本,瀏覽器類型,行為歷史記錄等。"
|
sendErrorReportsDescription: "開啟後,錯誤出現時將會與 Calckey 分享詳細紀錄,對於 Calckey 的開發會有非常大的幫助。\n
|
||||||
|
這將包括您的操作系統版本、使用的瀏覽器、您在 Calckey 中的活動等資料。"
|
||||||
myTheme: "我的佈景主題"
|
myTheme: "我的佈景主題"
|
||||||
backgroundColor: "背景"
|
backgroundColor: "背景"
|
||||||
accentColor: "重點色彩"
|
accentColor: "重點色彩"
|
||||||
|
@ -862,7 +863,7 @@ check: "檢查"
|
||||||
driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限"
|
driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限"
|
||||||
driveCapOverrideCaption: "如果指定0以下的值,就會被取消。"
|
driveCapOverrideCaption: "如果指定0以下的值,就會被取消。"
|
||||||
requireAdminForView: "必須以管理者帳號登入才可以檢視。"
|
requireAdminForView: "必須以管理者帳號登入才可以檢視。"
|
||||||
isSystemAccount: "由系統自動建立與管理的帳號。"
|
isSystemAccount: "該帳號由系統自動創建並運行。 千千萬萬不要審核、編輯、刪除或以其他方式修改此帳戶,否則可能會破壞您的伺服器。"
|
||||||
typeToConfirm: "要執行這項操作,請輸入 {x}"
|
typeToConfirm: "要執行這項操作,請輸入 {x}"
|
||||||
deleteAccount: "刪除帳號"
|
deleteAccount: "刪除帳號"
|
||||||
document: "文件"
|
document: "文件"
|
||||||
|
@ -1855,3 +1856,14 @@ hiddenTagsDescription: '列出您希望隱藏趨勢和探索的主題標籤(
|
||||||
userSaysSomethingReasonQuote: '{name} 引用了一篇包含 {reason} 的貼文'
|
userSaysSomethingReasonQuote: '{name} 引用了一篇包含 {reason} 的貼文'
|
||||||
silencedInstancesDescription: 列出您想要靜音的伺服器的網址。 您列出的伺服器內的帳戶將被視為“沉默”,只能發出追隨請求,如果不追隨則不能提及本地帳戶。
|
silencedInstancesDescription: 列出您想要靜音的伺服器的網址。 您列出的伺服器內的帳戶將被視為“沉默”,只能發出追隨請求,如果不追隨則不能提及本地帳戶。
|
||||||
這不會影響被阻止的伺服器。
|
這不會影響被阻止的伺服器。
|
||||||
|
video: 影片
|
||||||
|
audio: 音訊
|
||||||
|
sendPushNotificationReadMessageCaption: 包含文本 “{emptyPushNotificationMessage}” 的通知將顯示一小段時間。
|
||||||
|
這可能會增加您設備的電池使用量(如果適用)。
|
||||||
|
channelFederationWarn: 頻道功能尚未與聯邦宇宙連動
|
||||||
|
swipeOnMobile: 允許在頁面之間滑動
|
||||||
|
sendPushNotificationReadMessage: 閱讀相關通知或消息後刪除推送通知
|
||||||
|
image: 圖片
|
||||||
|
seperateRenoteQuote: 分別獨立的轉傳及引用按鈕
|
||||||
|
clipsDesc: 摘錄就像一個可以分享的書籤。 你可以從每個貼文的菜單創建新摘錄或將貼文加入已有的摘錄。
|
||||||
|
noteId: 貼文 ID
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
export class AnnouncementPopup1688845537045 {
|
||||||
|
name = "AnnouncementPopup1688845537045";
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "announcement" ADD "showPopup" boolean NOT NULL DEFAULT false`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "announcement" ADD "isGoodNews" boolean NOT NULL DEFAULT false`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "announcement" DROP COLUMN "isGoodNews"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "announcement" DROP COLUMN "showPopup"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,6 +36,16 @@ export class Announcement {
|
||||||
})
|
})
|
||||||
public imageUrl: string | null;
|
public imageUrl: string | null;
|
||||||
|
|
||||||
|
@Column("boolean", {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public showPopup: boolean;
|
||||||
|
|
||||||
|
@Column("boolean", {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public isGoodNews: boolean;
|
||||||
|
|
||||||
constructor(data: Partial<Announcement>) {
|
constructor(data: Partial<Announcement>) {
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,16 @@ export const meta = {
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
},
|
},
|
||||||
|
showPopup: {
|
||||||
|
type: "boolean",
|
||||||
|
optional: true,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
isGoodNews: {
|
||||||
|
type: "boolean",
|
||||||
|
optional: true,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -57,6 +67,8 @@ export const paramDef = {
|
||||||
title: { type: "string", minLength: 1 },
|
title: { type: "string", minLength: 1 },
|
||||||
text: { type: "string", minLength: 1 },
|
text: { type: "string", minLength: 1 },
|
||||||
imageUrl: { type: "string", nullable: true, minLength: 1 },
|
imageUrl: { type: "string", nullable: true, minLength: 1 },
|
||||||
|
showPopup: { type: "boolean" },
|
||||||
|
isGoodNews: { type: "boolean" },
|
||||||
},
|
},
|
||||||
required: ["title", "text", "imageUrl"],
|
required: ["title", "text", "imageUrl"],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -69,6 +81,8 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
title: ps.title,
|
title: ps.title,
|
||||||
text: ps.text,
|
text: ps.text,
|
||||||
imageUrl: ps.imageUrl,
|
imageUrl: ps.imageUrl,
|
||||||
|
showPopup: ps.showPopup ?? false,
|
||||||
|
isGoodNews: ps.isGoodNews ?? false,
|
||||||
}).then((x) => Announcements.findOneByOrFail(x.identifiers[0]));
|
}).then((x) => Announcements.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
return Object.assign({}, announcement, {
|
return Object.assign({}, announcement, {
|
||||||
|
|
|
@ -57,6 +57,16 @@ export const meta = {
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
|
showPopup: {
|
||||||
|
type: "boolean",
|
||||||
|
optional: true,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
isGoodNews: {
|
||||||
|
type: "boolean",
|
||||||
|
optional: true,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -100,5 +110,7 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
text: announcement.text,
|
text: announcement.text,
|
||||||
imageUrl: announcement.imageUrl,
|
imageUrl: announcement.imageUrl,
|
||||||
reads: reads.get(announcement)!,
|
reads: reads.get(announcement)!,
|
||||||
|
showPopup: announcement.showPopup,
|
||||||
|
isGoodNews: announcement.isGoodNews,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,6 +24,8 @@ export const paramDef = {
|
||||||
title: { type: "string", minLength: 1 },
|
title: { type: "string", minLength: 1 },
|
||||||
text: { type: "string", minLength: 1 },
|
text: { type: "string", minLength: 1 },
|
||||||
imageUrl: { type: "string", nullable: true, minLength: 1 },
|
imageUrl: { type: "string", nullable: true, minLength: 1 },
|
||||||
|
showPopup: { type: "boolean" },
|
||||||
|
isGoodNews: { type: "boolean" },
|
||||||
},
|
},
|
||||||
required: ["id", "title", "text", "imageUrl"],
|
required: ["id", "title", "text", "imageUrl"],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -38,5 +40,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
title: ps.title,
|
title: ps.title,
|
||||||
text: ps.text,
|
text: ps.text,
|
||||||
imageUrl: ps.imageUrl,
|
imageUrl: ps.imageUrl,
|
||||||
|
showPopup: ps.showPopup ?? false,
|
||||||
|
isGoodNews: ps.isGoodNews ?? false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -56,6 +56,16 @@ export const meta = {
|
||||||
optional: true,
|
optional: true,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
|
showPopup: {
|
||||||
|
type: "boolean",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
isGoodNews: {
|
||||||
|
type: "boolean",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -33,7 +33,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
if (ps.key !== "reactions") return;
|
if (ps.key !== "reactions" && ps.key !== "defaultNoteVisibility") return;
|
||||||
const query = RegistryItems.createQueryBuilder("item")
|
const query = RegistryItems.createQueryBuilder("item")
|
||||||
.where("item.domain IS NULL")
|
.where("item.domain IS NULL")
|
||||||
.andWhere("item.userId = :userId", { userId: user.id })
|
.andWhere("item.userId = :userId", { userId: user.id })
|
||||||
|
|
|
@ -112,7 +112,7 @@ mastoFileRouter.post("/v2/media", upload.single("file"), async (ctx) => {
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = await client.uploadMedia(multipartData);
|
const data = await client.uploadMedia(multipartData, ctx.request.body);
|
||||||
ctx.body = convertAttachment(data.data as Entity.Attachment);
|
ctx.body = convertAttachment(data.data as Entity.Attachment);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
@ -48,7 +48,7 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
acct.source = {
|
acct.source = {
|
||||||
note: acct.note,
|
note: acct.note,
|
||||||
fields: acct.fields,
|
fields: acct.fields,
|
||||||
privacy: "public",
|
privacy: await client.getDefaultPostPrivacy(),
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
language: "",
|
language: "",
|
||||||
};
|
};
|
||||||
|
|
|
@ -123,27 +123,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
id,
|
id,
|
||||||
convertTimelinesArgsId(limitToInt(ctx.query as any)),
|
convertTimelinesArgsId(limitToInt(ctx.query as any)),
|
||||||
);
|
);
|
||||||
const status = await client.getStatus(id);
|
|
||||||
let reqInstance = axios.create({
|
|
||||||
headers: {
|
|
||||||
Authorization: ctx.headers.authorization,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const reactionsAxios = await reqInstance.get(
|
|
||||||
`${BASE_URL}/api/notes/reactions?noteId=${id}`,
|
|
||||||
);
|
|
||||||
const reactions: IReaction[] = reactionsAxios.data;
|
|
||||||
const text = reactions
|
|
||||||
.map((r) => `${r.type.replace("@.", "")} ${r.user.username}`)
|
|
||||||
.join("<br />");
|
|
||||||
data.data.descendants.unshift(
|
|
||||||
statusModel(
|
|
||||||
status.data.id,
|
|
||||||
status.data.account.id,
|
|
||||||
status.data.emojis,
|
|
||||||
text,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
data.data.ancestors = data.data.ancestors.map((status) =>
|
data.data.ancestors = data.data.ancestors.map((status) =>
|
||||||
convertStatus(status),
|
convertStatus(status),
|
||||||
);
|
);
|
||||||
|
@ -456,65 +436,3 @@ async function getFirstReaction(
|
||||||
return react;
|
return react;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function statusModel(
|
|
||||||
id: string | null,
|
|
||||||
acctId: string | null,
|
|
||||||
emojis: MastodonEntity.Emoji[],
|
|
||||||
content: string,
|
|
||||||
) {
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
return {
|
|
||||||
id: "9atm5frjhb",
|
|
||||||
uri: "/static-assets/transparent.png", // ""
|
|
||||||
url: "/static-assets/transparent.png", // "",
|
|
||||||
account: {
|
|
||||||
id: "9arzuvv0sw",
|
|
||||||
username: "Reactions",
|
|
||||||
acct: "Reactions",
|
|
||||||
display_name: "Reactions to this post",
|
|
||||||
locked: false,
|
|
||||||
created_at: now,
|
|
||||||
followers_count: 0,
|
|
||||||
following_count: 0,
|
|
||||||
statuses_count: 0,
|
|
||||||
note: "",
|
|
||||||
url: "/static-assets/transparent.png",
|
|
||||||
avatar: "/static-assets/badges/info.png",
|
|
||||||
avatar_static: "/static-assets/badges/info.png",
|
|
||||||
header: "/static-assets/transparent.png", // ""
|
|
||||||
header_static: "/static-assets/transparent.png", // ""
|
|
||||||
emojis: [],
|
|
||||||
fields: [],
|
|
||||||
moved: null,
|
|
||||||
bot: false,
|
|
||||||
},
|
|
||||||
in_reply_to_id: id,
|
|
||||||
in_reply_to_account_id: acctId,
|
|
||||||
reblog: null,
|
|
||||||
content: `<p>${content}</p>`,
|
|
||||||
plain_content: null,
|
|
||||||
created_at: now,
|
|
||||||
emojis: emojis,
|
|
||||||
replies_count: 0,
|
|
||||||
reblogs_count: 0,
|
|
||||||
favourites_count: 0,
|
|
||||||
favourited: false,
|
|
||||||
reblogged: false,
|
|
||||||
muted: false,
|
|
||||||
sensitive: false,
|
|
||||||
spoiler_text: "",
|
|
||||||
visibility: "public" as const,
|
|
||||||
media_attachments: [],
|
|
||||||
mentions: [],
|
|
||||||
tags: [],
|
|
||||||
card: null,
|
|
||||||
poll: null,
|
|
||||||
application: null,
|
|
||||||
language: null,
|
|
||||||
pinned: false,
|
|
||||||
emoji_reactions: [],
|
|
||||||
bookmarked: false,
|
|
||||||
quote: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
74
packages/client/src/components/MkAnnouncement.vue
Normal file
74
packages/client/src/components/MkAnnouncement.vue
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<template>
|
||||||
|
<MkModal ref="modal" :z-priority="'middle'" @closed="$emit('closed')">
|
||||||
|
<div :class="$style.root">
|
||||||
|
<div :class="$style.title">
|
||||||
|
<MkSparkle v-if="isGoodNews">{{ title }}</MkSparkle>
|
||||||
|
<p v-else>{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
<Mfm :text="text" />
|
||||||
|
<img
|
||||||
|
v-if="imageUrl != null"
|
||||||
|
:key="imageUrl"
|
||||||
|
:src="imageUrl"
|
||||||
|
alt="attached image"
|
||||||
|
/>
|
||||||
|
<MkButton :class="$style.gotIt" primary full @click="gotIt()">{{
|
||||||
|
i18n.ts.gotIt
|
||||||
|
}}</MkButton>
|
||||||
|
</div>
|
||||||
|
</MkModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { shallowRef } from "vue";
|
||||||
|
import MkModal from "@/components/MkModal.vue";
|
||||||
|
import MkSparkle from "@/components/MkSparkle.vue";
|
||||||
|
import MkButton from "@/components/MkButton.vue";
|
||||||
|
import { i18n } from "@/i18n";
|
||||||
|
import * as os from "@/os";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
announcement: Announcement;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { id, text, title, imageUrl, isGoodNews } = props.announcement;
|
||||||
|
|
||||||
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
|
||||||
|
const gotIt = () => {
|
||||||
|
modal.value.close();
|
||||||
|
os.api("i/read-announcement", { announcementId: id });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
margin: auto;
|
||||||
|
position: relative;
|
||||||
|
padding: 32px;
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 480px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
|
||||||
|
> img {
|
||||||
|
border-radius: 10px;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gotIt {
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
}
|
||||||
|
</style>
|
53
packages/client/src/components/MkManyAnnouncements.vue
Normal file
53
packages/client/src/components/MkManyAnnouncements.vue
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<MkModal ref="modal" :z-priority="'middle'" @closed="$emit('closed')">
|
||||||
|
<div :class="$style.root">
|
||||||
|
<p :class="$style.title">
|
||||||
|
{{ i18n.ts.youHaveUnreadAnnouncements }}
|
||||||
|
</p>
|
||||||
|
<MkButton
|
||||||
|
:class="$style.gotIt"
|
||||||
|
primary
|
||||||
|
full
|
||||||
|
@click="checkAnnouncements()"
|
||||||
|
>{{ i18n.ts.gotIt }}</MkButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</MkModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { shallowRef } from "vue";
|
||||||
|
import MkModal from "@/components/MkModal.vue";
|
||||||
|
import MkButton from "@/components/MkButton.vue";
|
||||||
|
import { i18n } from "@/i18n";
|
||||||
|
import * as os from "@/os";
|
||||||
|
|
||||||
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
const checkAnnouncements = () => {
|
||||||
|
modal.value.close();
|
||||||
|
location.href = "/announcements";
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
margin: auto;
|
||||||
|
position: relative;
|
||||||
|
padding: 32px;
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 480px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gotIt {
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -36,7 +36,7 @@ import { version, ui, lang, host } from "@/config";
|
||||||
import { applyTheme } from "@/scripts/theme";
|
import { applyTheme } from "@/scripts/theme";
|
||||||
import { isDeviceDarkmode } from "@/scripts/is-device-darkmode";
|
import { isDeviceDarkmode } from "@/scripts/is-device-darkmode";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import { confirm, alert, post, popup, toast } from "@/os";
|
import { confirm, alert, post, popup, toast, api } from "@/os";
|
||||||
import { stream } from "@/stream";
|
import { stream } from "@/stream";
|
||||||
import * as sound from "@/scripts/sound";
|
import * as sound from "@/scripts/sound";
|
||||||
import { $i, refreshAccount, login, updateAccount, signout } from "@/account";
|
import { $i, refreshAccount, login, updateAccount, signout } from "@/account";
|
||||||
|
@ -272,6 +272,42 @@ function checkForSplash() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$i &&
|
||||||
|
defaultStore.state.tutorial === -1 &&
|
||||||
|
!["/announcements", "/announcements/"].includes(window.location.pathname)
|
||||||
|
) {
|
||||||
|
api("announcements", { withUnreads: true, limit: 10 })
|
||||||
|
.then((announcements) => {
|
||||||
|
const unreadAnnouncements = announcements.filter((item) => {
|
||||||
|
return !item.isRead;
|
||||||
|
});
|
||||||
|
if (unreadAnnouncements.length > 3) {
|
||||||
|
popup(
|
||||||
|
defineAsyncComponent(
|
||||||
|
() => import("@/components/MkManyAnnouncements.vue"),
|
||||||
|
),
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
"closed",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
unreadAnnouncements.forEach((item) => {
|
||||||
|
if (item.showPopup)
|
||||||
|
popup(
|
||||||
|
defineAsyncComponent(
|
||||||
|
() => import("@/components/MkAnnouncement.vue"),
|
||||||
|
),
|
||||||
|
{ announcement: item },
|
||||||
|
{},
|
||||||
|
"closed",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => console.log(err));
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため)
|
// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため)
|
||||||
watch(
|
watch(
|
||||||
defaultStore.reactiveState.darkMode,
|
defaultStore.reactiveState.darkMode,
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
:display-back-button="true"
|
:display-back-button="true"
|
||||||
/></template>
|
/></template>
|
||||||
<MkSpacer :content-max="900">
|
<MkSpacer :content-max="900">
|
||||||
<div class="ztgjmzrw">
|
<div :class="$style.root">
|
||||||
<section
|
<section
|
||||||
v-for="announcement in announcements"
|
v-for="announcement in announcements"
|
||||||
class="_card _gap announcements"
|
class="_card _gap announcements"
|
||||||
|
@ -22,6 +22,17 @@
|
||||||
<MkInput v-model="announcement.imageUrl">
|
<MkInput v-model="announcement.imageUrl">
|
||||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
<MkSwitch
|
||||||
|
v-model="announcement.showPopup"
|
||||||
|
class="_formBlock"
|
||||||
|
>{{ i18n.ts.showPopup }}</MkSwitch
|
||||||
|
>
|
||||||
|
<MkSwitch
|
||||||
|
v-if="announcement.showPopup"
|
||||||
|
v-model="announcement.isGoodNews"
|
||||||
|
class="_formBlock"
|
||||||
|
>{{ i18n.ts.showWithSparkles }}</MkSwitch
|
||||||
|
>
|
||||||
<p v-if="announcement.reads">
|
<p v-if="announcement.reads">
|
||||||
{{
|
{{
|
||||||
i18n.t("nUsersRead", { n: announcement.reads })
|
i18n.t("nUsersRead", { n: announcement.reads })
|
||||||
|
@ -57,6 +68,7 @@
|
||||||
import {} from "vue";
|
import {} from "vue";
|
||||||
import MkButton from "@/components/MkButton.vue";
|
import MkButton from "@/components/MkButton.vue";
|
||||||
import MkInput from "@/components/form/input.vue";
|
import MkInput from "@/components/form/input.vue";
|
||||||
|
import MkSwitch from "@/components/form/switch.vue";
|
||||||
import MkTextarea from "@/components/form/textarea.vue";
|
import MkTextarea from "@/components/form/textarea.vue";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
@ -74,6 +86,8 @@ function add() {
|
||||||
title: "",
|
title: "",
|
||||||
text: "",
|
text: "",
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
|
showPopup: false,
|
||||||
|
isGoodNews: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,8 +151,8 @@ definePageMetadata({
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.ztgjmzrw {
|
.root {
|
||||||
margin: var(--margin);
|
margin: var(--margin);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -460,7 +460,7 @@ export default class Misskey implements MegalodonInterface {
|
||||||
if (options) {
|
if (options) {
|
||||||
if (options.limit) {
|
if (options.limit) {
|
||||||
params = Object.assign(params, {
|
params = Object.assign(params, {
|
||||||
limit: options.limit
|
limit: options.limit <= 100 ? options.limit : 100
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -474,11 +474,11 @@ export default class Misskey implements MegalodonInterface {
|
||||||
limit: 40
|
limit: 40
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return this.client.post<Array<MisskeyAPI.Entity.Follower>>('/api/users/followers', params).then(res => {
|
return this.client.post<Array<MisskeyAPI.Entity.Follower>>('/api/users/followers', params).then(async res => {
|
||||||
return Object.assign(res, {
|
return Object.assign(res, {
|
||||||
data: res.data.map(f => this.converter.follower(f))
|
data: (await Promise.all(res.data.map(async f => (this.getAccount(f.followerId)).then(p => p.data))))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -498,15 +498,15 @@ export default class Misskey implements MegalodonInterface {
|
||||||
if (options) {
|
if (options) {
|
||||||
if (options.limit) {
|
if (options.limit) {
|
||||||
params = Object.assign(params, {
|
params = Object.assign(params, {
|
||||||
limit: options.limit
|
limit: options.limit <= 100 ? options.limit : 100
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.client.post<Array<MisskeyAPI.Entity.Following>>('/api/users/following', params).then(res => {
|
return this.client.post<Array<MisskeyAPI.Entity.Following>>('/api/users/following', params).then(async res => {
|
||||||
return Object.assign(res, {
|
return Object.assign(res, {
|
||||||
data: res.data.map(f => this.converter.following(f))
|
data: (await Promise.all(res.data.map(async f => (this.getAccount(f.followeeId)).then(p => p.data))))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAccountLists(_id: string): Promise<Response<Array<Entity.List>>> {
|
public async getAccountLists(_id: string): Promise<Response<Array<Entity.List>>> {
|
||||||
|
@ -1079,23 +1079,11 @@ export default class Misskey implements MegalodonInterface {
|
||||||
// accounts/preferences
|
// accounts/preferences
|
||||||
// ======================================
|
// ======================================
|
||||||
public async getPreferences(): Promise<Response<Entity.Preferences>> {
|
public async getPreferences(): Promise<Response<Entity.Preferences>> {
|
||||||
return this.client.post<MisskeyAPI.Entity.UserDetailMe>('/api/i').then(res => {
|
return this.client.post<MisskeyAPI.Entity.UserDetailMe>('/api/i').then(async res => {
|
||||||
/*
|
return Object.assign(res, {
|
||||||
return this.client.post<MisskeyAPI.Entity.GetAll>('/api/i/registry/get-all', {
|
data: this.converter.userPreferences(res.data, await this.getDefaultPostPrivacy())
|
||||||
scope: ['client', 'base'],
|
})
|
||||||
}).then(ga => {
|
})
|
||||||
return Object.assign(res, {
|
|
||||||
data: this.converter.userPreferences(res.data, ga.data)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// FIXME: get this from api
|
|
||||||
return Object.assign(res, {
|
|
||||||
data: this.converter.userPreferences(res.data, {defaultNoteVisibility: "followers", tutorial: -1})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================================
|
// ======================================
|
||||||
|
@ -1529,6 +1517,23 @@ export default class Misskey implements MegalodonInterface {
|
||||||
.then(res => res.data[0] ?? '⭐');
|
.then(res => res.data[0] ?? '⭐');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getDefaultPostPrivacy(): Promise<'public' | 'unlisted' | 'private' | 'direct'> {
|
||||||
|
// NOTE: get-unsecure is calckey's extension.
|
||||||
|
// Misskey doesn't have this endpoint and regular `/i/registry/get` won't work
|
||||||
|
// unless you have a 'nativeToken', which is reserved for the frontend webapp.
|
||||||
|
|
||||||
|
return this.client
|
||||||
|
.post<string>('/api/i/registry/get-unsecure', {
|
||||||
|
key: 'defaultNoteVisibility',
|
||||||
|
scope: ['client', 'base'],
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (!res.data || (res.data != 'public' && res.data != 'home' && res.data != 'followers' && res.data != 'specified'))
|
||||||
|
return 'public';
|
||||||
|
return this.converter.visibility(res.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async unfavouriteStatus(id: string): Promise<Response<Entity.Status>> {
|
public async unfavouriteStatus(id: string): Promise<Response<Entity.Status>> {
|
||||||
// NOTE: Misskey allows only one reaction per status, so we don't need to care what that emoji was.
|
// NOTE: Misskey allows only one reaction per status, so we don't need to care what that emoji was.
|
||||||
return this.deleteEmojiReaction(id, '');
|
return this.deleteEmojiReaction(id, '');
|
||||||
|
@ -1638,20 +1643,26 @@ export default class Misskey implements MegalodonInterface {
|
||||||
/**
|
/**
|
||||||
* POST /api/drive/files/create
|
* POST /api/drive/files/create
|
||||||
*/
|
*/
|
||||||
public async uploadMedia(file: any, _options?: { description?: string; focus?: string }): Promise<Response<Entity.Attachment>> {
|
public async uploadMedia(file: any, options?: { description?: string; focus?: string }): Promise<Response<Entity.Attachment>> {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', fs.createReadStream(file.path), {
|
formData.append('file', fs.createReadStream(file.path), {
|
||||||
contentType: file.mimetype,
|
contentType: file.mimetype
|
||||||
filename: file.originalname,
|
})
|
||||||
})
|
|
||||||
|
if (file.originalname != null && file.originalname !== 'file')
|
||||||
|
formData.append('name', file.originalname);
|
||||||
|
|
||||||
|
if (options?.description != null)
|
||||||
|
formData.append('comment', options.description);
|
||||||
|
|
||||||
let headers: { [key: string]: string } = {}
|
let headers: { [key: string]: string } = {}
|
||||||
if (typeof formData.getHeaders === 'function') {
|
if (typeof formData.getHeaders === 'function') {
|
||||||
headers = formData.getHeaders()
|
headers = formData.getHeaders()
|
||||||
}
|
}
|
||||||
return this.client
|
return this.client
|
||||||
.post<MisskeyAPI.Entity.File>('/api/drive/files/create', formData, headers)
|
.post<MisskeyAPI.Entity.File>('/api/drive/files/create', formData, headers)
|
||||||
.then(res => ({ ...res, data: this.converter.file(res.data) }))
|
.then(res => ({ ...res, data: this.converter.file(res.data) }))
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMedia(id: string): Promise<Response<Entity.Attachment>> {
|
public async getMedia(id: string): Promise<Response<Entity.Attachment>> {
|
||||||
const res = await this.client.post<MisskeyAPI.Entity.File>('/api/drive/files/show', { fileId: id })
|
const res = await this.client.post<MisskeyAPI.Entity.File>('/api/drive/files/show', { fileId: id })
|
||||||
|
@ -1679,6 +1690,12 @@ export default class Misskey implements MegalodonInterface {
|
||||||
isSensitive: options.is_sensitive
|
isSensitive: options.is_sensitive
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.description !== undefined) {
|
||||||
|
params = Object.assign(params, {
|
||||||
|
comment: options.description
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return this.client
|
return this.client
|
||||||
.post<MisskeyAPI.Entity.File>('/api/drive/files/update', params)
|
.post<MisskeyAPI.Entity.File>('/api/drive/files/update', params)
|
||||||
|
|
|
@ -134,8 +134,8 @@ namespace MisskeyAPI {
|
||||||
url: acctUrl,
|
url: acctUrl,
|
||||||
avatar: u.avatarUrl,
|
avatar: u.avatarUrl,
|
||||||
avatar_static: u.avatarUrl,
|
avatar_static: u.avatarUrl,
|
||||||
header: this.plcUrl, // FIXME
|
header: this.plcUrl,
|
||||||
header_static: this.plcUrl, // FIXME
|
header_static: this.plcUrl,
|
||||||
emojis: u.emojis.map(e => this.emoji(e)),
|
emojis: u.emojis.map(e => this.emoji(e)),
|
||||||
moved: null,
|
moved: null,
|
||||||
fields: [],
|
fields: [],
|
||||||
|
@ -174,13 +174,13 @@ namespace MisskeyAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
userPreferences = (u: MisskeyAPI.Entity.UserDetailMe, g: MisskeyAPI.Entity.GetAll): MegalodonEntity.Preferences => {
|
userPreferences = (u: MisskeyAPI.Entity.UserDetailMe, v: 'public' | 'unlisted' | 'private' | 'direct'): MegalodonEntity.Preferences => {
|
||||||
return {
|
return {
|
||||||
"reading:expand:media": "default",
|
"reading:expand:media": "default",
|
||||||
"reading:expand:spoilers": false,
|
"reading:expand:spoilers": false,
|
||||||
"posting:default:language": u.lang,
|
"posting:default:language": u.lang,
|
||||||
"posting:default:sensitive": u.alwaysMarkNsfw,
|
"posting:default:sensitive": u.alwaysMarkNsfw,
|
||||||
"posting:default:visibility": this.visibility(g.defaultNoteVisibility)
|
"posting:default:visibility": v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -308,7 +308,7 @@ namespace MisskeyAPI {
|
||||||
emojis: n.emojis.map(e => this.emoji(e)),
|
emojis: n.emojis.map(e => this.emoji(e)),
|
||||||
replies_count: n.repliesCount,
|
replies_count: n.repliesCount,
|
||||||
reblogs_count: n.renoteCount,
|
reblogs_count: n.renoteCount,
|
||||||
favourites_count: this.getTotalReactions(n.reactions), // FIXME: instead get # of default reaction emoji reactions
|
favourites_count: this.getTotalReactions(n.reactions),
|
||||||
reblogged: false,
|
reblogged: false,
|
||||||
favourited: !!n.myReaction,
|
favourited: !!n.myReaction,
|
||||||
muted: false,
|
muted: false,
|
||||||
|
|
Loading…
Reference in a new issue