Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
90a9cf376e | |||
16d6c55407 | |||
f3508d15a3 | |||
0add490097 | |||
2d2d1bd58d | |||
7813c8a942 | |||
ac5453232f | |||
e78e5274d3 | |||
982520bcef | |||
7abd91f031 | |||
b93bfb7e5c | |||
2b20c34c1e | |||
0f63acea5b | |||
e600fb7096 | |||
b63fc71865 | |||
23e7650983 | |||
01b5ccfdc6 | |||
cc72f91465 | |||
45cb5ff4ef | |||
390279a4a8 | |||
851dececab | |||
482afa93a2 | |||
25bdbd7ae0 | |||
527a639242 | |||
0d5e000ad3 | |||
f4cb467e7a | |||
2f6b0b142a | |||
78c2535c3c | |||
26260392a8 | |||
aa573c0063 | |||
b2859bcd2a |
31
CHANGELOG.md
31
CHANGELOG.md
@ -1,6 +1,37 @@
|
||||
ChangeLog
|
||||
=========
|
||||
|
||||
12.5.0 (2020/02/09)
|
||||
--------------------
|
||||
### ✨Improvements
|
||||
* チュートリアルを実装
|
||||
* 検索のキーボードショートカットを追加
|
||||
* タイムラインを遡っている状況でないときに、誰かをフォローまたはフォロー解除したときにタイムラインをリロードするように
|
||||
|
||||
### 🐛Fixes
|
||||
* グループチャットが開始できない問題を修正
|
||||
* Renoteメニューが開けない問題を修正
|
||||
* 誕生日設定が崩れていたのを修正
|
||||
* キャッシュが削除できない問題を修正
|
||||
|
||||
12.4.1 (2020/02/09)
|
||||
--------------------
|
||||
### 🐛Fixes
|
||||
* グループの招待をacceptもrejectも出来ない問題を修正
|
||||
* 非ログイン時に検索欄がズレていたのを修正
|
||||
* バックグラウンドで受信したタイムラインの投稿のリアクションが受信されていない問題を修正
|
||||
|
||||
12.4.0 (2020/02/09)
|
||||
--------------------
|
||||
### ✨Improvements
|
||||
* ローカルのみをデフォルトで操作できるように
|
||||
* キーボード操作を改善
|
||||
* AP: Create ActivityでattributedToの補完とaudienceのコピーを行うように
|
||||
|
||||
### 🐛Fixes
|
||||
* ページ遷移してもナビゲーションが閉じない問題を修正
|
||||
* デフォルトの公開範囲のリストにホームがなかったので復活
|
||||
|
||||
12.3.0 (2020/02/08)
|
||||
--------------------
|
||||
### ✨Improvements
|
||||
|
@ -360,6 +360,7 @@ markAsReadAllTalkMessages: "Mark all conversations as read"
|
||||
help: "Help"
|
||||
inputMessageHere: "Enter message here"
|
||||
close: "Close"
|
||||
group: "Groups"
|
||||
groups: "Groups"
|
||||
createGroup: "Create a group"
|
||||
ownedGroups: "Owned Groups"
|
||||
|
@ -215,7 +215,7 @@ remove: "削除"
|
||||
removed: "削除しました"
|
||||
removeAreYouSure: "「{x}」を削除しますか?"
|
||||
saved: "保存しました"
|
||||
messaging: "トーク"
|
||||
messaging: "チャット"
|
||||
upload: "アップロード"
|
||||
fromDrive: "ドライブから"
|
||||
fromUrl: "URLから"
|
||||
@ -226,7 +226,7 @@ games: "Misskey Games"
|
||||
messageRead: "既読"
|
||||
recentUsedEmojis: "最近使用した絵文字"
|
||||
noMoreHistory: "これより過去の履歴はありません"
|
||||
startMessaging: "トークを開始"
|
||||
startMessaging: "チャットを開始"
|
||||
nUsersRead: "{n}人が読みました"
|
||||
agreeTo: "{0}に同意"
|
||||
tos: "利用規約"
|
||||
@ -349,6 +349,7 @@ post: "投稿"
|
||||
posted: "投稿しました"
|
||||
autoReloadWhenDisconnected: "サーバー切断時に自動リロード"
|
||||
autoNoteWatch: "ノートの自動ウォッチ"
|
||||
autoNoteWatchDescription: "あなたがリアクションしたり返信したりした他のユーザーのノートに関する通知を受け取るようにします。"
|
||||
reduceUiAnimation: "UIのアニメーションを減らす"
|
||||
share: "共有"
|
||||
notFound: "見つかりません"
|
||||
@ -357,10 +358,11 @@ uploadFolder: "既定アップロード先"
|
||||
cacheClear: "キャッシュを削除"
|
||||
markAsReadAllNotifications: "すべての通知を既読にする"
|
||||
markAsReadAllUnreadNotes: "すべての投稿を既読にする"
|
||||
markAsReadAllTalkMessages: "すべてのトークを既読にする"
|
||||
markAsReadAllTalkMessages: "すべてのチャットを既読にする"
|
||||
help: "ヘルプ"
|
||||
inputMessageHere: "ここにメッセージを入力"
|
||||
close: "閉じる"
|
||||
group: "グループ"
|
||||
groups: "グループ"
|
||||
createGroup: "グループを作成"
|
||||
ownedGroups: "所有グループ"
|
||||
@ -369,6 +371,35 @@ invites: "招待"
|
||||
groupName: "グループ名"
|
||||
members: "メンバー"
|
||||
transfer: "譲渡"
|
||||
messagingWithUser: "ユーザーとチャット"
|
||||
messagingWithGroup: "グループでチャット"
|
||||
enable: "有効にする"
|
||||
next: "次"
|
||||
retype: "再入力"
|
||||
|
||||
_tutorial:
|
||||
title: "Misskeyの使い方"
|
||||
step1_1: "ようこそ。"
|
||||
step1_2: "この画面は「タイムライン」と呼ばれ、あなたや、あなたが「フォロー」する人の「ノート」が時系列で表示されます。"
|
||||
step1_3: "あなたはまだ何もノートを投稿しておらず、誰もフォローしていないので、タイムラインには何も表示されていないはずです。"
|
||||
step2_1: "ノートを作成したり誰かをフォローしたりする前に、まずあなたのプロフィールを完成させましょう。"
|
||||
step2_2: "あなたがどんな人かわかると、多くの人にノートを見てもらえたり、フォローしてもらいやすくなります。"
|
||||
step3_1: "プロフィール設定はうまくできましたか?"
|
||||
step3_2: "では試しに、何かノートを投稿してみてください。画面上にある鉛筆マークのボタンを押すとフォームが開きます。"
|
||||
step3_3: "内容を書いたら、フォーム右上のボタンを押すと投稿できます。"
|
||||
step3_4: "内容が思いつかない?「Misskey始めました」というのはいかがでしょう。"
|
||||
step4_1: "投稿できましたか?"
|
||||
step4_2: "あなたのノートがタイムラインに表示されていれば成功です。"
|
||||
step5_1: "次は、他の人をフォローしてタイムラインを賑やかにしたいところです。"
|
||||
step5_2: "{featured}で人気のノートが見れるので、その中から気になった人を選んでフォローしたり、{explore}で人気のユーザーを探すこともできます。"
|
||||
step5_3: "ユーザーをフォローするには、ユーザーのアイコンをクリックしてユーザーページを表示し、「フォロー」ボタンを押します。"
|
||||
step5_4: "ユーザーによっては、フォローが承認されるまで時間がかかる場合があります。"
|
||||
step6_1: "タイムラインに他のユーザーのノートが表示されていれば成功です。"
|
||||
step6_2: "他の人のノートには、「リアクション」を付けることができ、簡単にあなたの反応を伝えられます。"
|
||||
step6_3: "リアクションを付けるには、ノートの「+」マークをクリックして、好きなリアクションを選択します。"
|
||||
step7_1: "これで、Misskeyの基本的な使い方の説明は終わりました。お疲れ様でした。"
|
||||
step7_2: "もっとMisskeyについて知りたいときは、{help}を見てみてください。"
|
||||
step7_3: "では、Misskeyをお楽しみください🚀"
|
||||
|
||||
_2fa:
|
||||
alreadyRegistered: "既に設定は完了しています。"
|
||||
@ -391,8 +422,8 @@ _permissions:
|
||||
"write:favorites": "お気に入りを操作する"
|
||||
"read:following": "フォローの情報を見る"
|
||||
"write:following": "フォロー・フォロー解除する"
|
||||
"read:messaging": "トークを見る"
|
||||
"write:messaging": "トークを操作する"
|
||||
"read:messaging": "チャットを見る"
|
||||
"write:messaging": "チャットを操作する"
|
||||
"read:mutes": "ミュートを見る"
|
||||
"write:mutes": "ミュートを操作する"
|
||||
"write:notes": "ノートを作成・削除する"
|
||||
@ -475,6 +506,7 @@ _visibility:
|
||||
followersDescription: "自分のフォロワーのみに公開"
|
||||
specified: "ダイレクト"
|
||||
specifiedDescription: "指定したユーザーのみに公開"
|
||||
localOnly: "ローカルのみ"
|
||||
|
||||
_postForm:
|
||||
replyPlaceholder: "このノートに返信..."
|
||||
|
@ -360,6 +360,7 @@ markAsReadAllTalkMessages: "모든 대화를 읽은 상태로 표시"
|
||||
help: "도움말"
|
||||
inputMessageHere: "여기에 메시지를 입력하세요"
|
||||
close: "닫기"
|
||||
group: "그룹"
|
||||
groups: "그룹"
|
||||
createGroup: "그룹 만들기"
|
||||
ownedGroups: "소유 그룹"
|
||||
|
@ -64,8 +64,14 @@ noLists: "列表为空"
|
||||
following: "关注中"
|
||||
followers: "关注者"
|
||||
followsYou: "关注了你"
|
||||
createList: "创建列表"
|
||||
manageLists: "管理列表"
|
||||
error: "有点小问题"
|
||||
retry: "重试"
|
||||
enterListName: "输入列表名称"
|
||||
privacy: "隐私"
|
||||
makeFollowManuallyApprove: "关注者请求需要批准"
|
||||
defaultNoteVisibility: "默认可见性"
|
||||
follow: "关注"
|
||||
followRequest: "关注申请"
|
||||
followRequests: "关注申请"
|
||||
@ -101,12 +107,18 @@ emojiName: "Emoji 名称"
|
||||
emojiUrl: "emoji 地址"
|
||||
addEmoji: "添加Emoji"
|
||||
cacheRemoteFiles: "远程文件缓存"
|
||||
flagAsBot: "这个账户是Bot"
|
||||
flagAsCat: "这个账户是Cat"
|
||||
addAcount: "添加账户"
|
||||
loginFailed: "登录失败"
|
||||
general: "常规设置"
|
||||
wallpaper: "壁纸"
|
||||
removeWallpaper: "移除壁纸"
|
||||
searchWith: "搜索:{q}"
|
||||
host: "主机名"
|
||||
selectUser: "选择用户"
|
||||
recipient: "收件人"
|
||||
annotation: "注解"
|
||||
federation: "联合"
|
||||
instances: "实例"
|
||||
latestRequestSentAt: "上次发送的请求"
|
||||
@ -115,6 +127,9 @@ storageUsage: "已用存储"
|
||||
perHour: "每小时"
|
||||
perDay: "每天"
|
||||
operations: "操作"
|
||||
software: "软件"
|
||||
version: "版本"
|
||||
metadata: "元数据"
|
||||
monitor: "监视器"
|
||||
jobQueue: "作业队列"
|
||||
cpuAndMemory: "CPU使用量"
|
||||
@ -122,6 +137,8 @@ network: "网络"
|
||||
disk: "存储"
|
||||
statistics: "统计"
|
||||
clearQueue: "清除队列"
|
||||
clearCachedFiles: "清除缓存"
|
||||
muteAndBlock: "屏蔽/拉黑"
|
||||
mutedUsers: "禁言用户"
|
||||
blockedUsers: "已屏蔽用户"
|
||||
noUsers: "无用户"
|
||||
@ -130,6 +147,10 @@ done: "完成"
|
||||
processing: "处理中"
|
||||
preview: "预览"
|
||||
noCustomEmojis: "无自定义Emoji"
|
||||
federating: "联合中"
|
||||
blocked: "已拦截"
|
||||
all: "全部"
|
||||
subscribing: "已订阅"
|
||||
notResponding: "没有响应"
|
||||
instanceFollowing: "关注实例"
|
||||
instanceFollowers: "关注实例"
|
||||
@ -143,13 +164,25 @@ attachFile: "插入附件"
|
||||
more: "更多!"
|
||||
featured: "高亮"
|
||||
lookup: "查询"
|
||||
imageUrl: "图片URL"
|
||||
remove: "删除"
|
||||
removed: "已删除"
|
||||
removeAreYouSure: "要删掉「{x}」吗?"
|
||||
saved: "已保存"
|
||||
messaging: "聊天"
|
||||
upload: "上传"
|
||||
fromUrl: "从 URL"
|
||||
editWidgets: "编辑部件"
|
||||
exitEdit: "停止编辑"
|
||||
explore: "发现"
|
||||
games: "Misskey游戏"
|
||||
messageRead: "已读"
|
||||
recentUsedEmojis: "最近使用的Emoji表情"
|
||||
noMoreHistory: "没有更多的历史记录"
|
||||
tos: "服务条款"
|
||||
start: "开始"
|
||||
home: "首页"
|
||||
activity: "活动"
|
||||
images: "图片"
|
||||
birthday: "生日"
|
||||
yearsOld: "{age}岁"
|
||||
@ -157,7 +190,23 @@ registeredDate: "注册于"
|
||||
location: "位置"
|
||||
theme: "主题"
|
||||
lightThemes: "亮色主题"
|
||||
darkThemes: "暗色主题"
|
||||
drive: "网盘"
|
||||
selectFile: "选择文件"
|
||||
selectFiles: "选择文件"
|
||||
renameFolder: "重命名文件夹"
|
||||
createFolder: "创建文件夹"
|
||||
deleteFolder: "删除文件夹"
|
||||
addFile: "添加文件"
|
||||
emptyDrive: "驱动器为空"
|
||||
emptyFolder: "空文件夹"
|
||||
copyUrl: "复制链接"
|
||||
rename: "重命名"
|
||||
avatar: "头像"
|
||||
banner: "Banner"
|
||||
nsfw: "阅读注意"
|
||||
disconnectedFromServer: "已从服务器断开连接"
|
||||
reloadConfirm: "确定要重新加载吗"
|
||||
accept: "允许"
|
||||
reject: "拒绝"
|
||||
instanceName: "实例名称"
|
||||
@ -181,23 +230,83 @@ registration: "注册"
|
||||
enableRegistration: "允许新用户注册"
|
||||
invite: "邀请"
|
||||
proxyRemoteFiles: "代理远程文件"
|
||||
proxyRemoteFilesDescription: "启用此设置后,由于超出存储容量而导致未保存被删除的远程文件将被本地代理,并且会生成缩略图。不会影响服务器的存储。"
|
||||
driveCapacityPerLocalAccount: "每个用户的网盘空间"
|
||||
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
|
||||
inMb: "以兆字节(Mbps)为单位"
|
||||
iconUrl: "图标URL"
|
||||
bannerUrl: "Banner URL"
|
||||
basicInfo: "基本信息"
|
||||
pinnedUsers: "置顶用户"
|
||||
recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
|
||||
recaptchaSiteKey: "网站密钥"
|
||||
recaptchaSecretKey: "reCAPTCHA 密钥"
|
||||
name: "名称"
|
||||
serviceworker: "ServiceWorker"
|
||||
enableServiceworker: "启用ServiceWorker"
|
||||
caseSensitive: "区分大小写"
|
||||
connectedTo: "您的账号已连到接以下社交账号"
|
||||
notesAndReplies: "帖子与回复"
|
||||
withFiles: "附件"
|
||||
silence: "禁言"
|
||||
silenceConfirm: "确认要禁言吗?"
|
||||
unsilenceConfirm: "要解除禁言吗?"
|
||||
popularUsers: "热门用户"
|
||||
recentlyUpdatedUsers: "最近投稿用户"
|
||||
recentlyRegisteredUsers: "最近登录用户"
|
||||
recentlyDiscoveredUsers: "最近发现的用户"
|
||||
popularTags: "热门标签"
|
||||
userList: "列表"
|
||||
about: "关于"
|
||||
aboutMisskey: "关于 Misskey"
|
||||
aboutMisskeyText: "Misskey是由syuilo于2014年开发的开放源代码软件。"
|
||||
misskeyMembers: "现在由以下成员进行开发和维护:"
|
||||
misskeySource: "源代码在这里公开:"
|
||||
misskeyDonate: "可以向 Misskey 进行捐款以支持开发:"
|
||||
morePatrons: "还有很多其他的人也在支持我们,非常感谢🥰"
|
||||
patrons: "支持者"
|
||||
administrator: "管理员"
|
||||
token: "令牌"
|
||||
twoStepAuthentication: "两步验证"
|
||||
moderator: "版主"
|
||||
nUsersMentioned: "{n} 被提到"
|
||||
securityKey: "安全密钥"
|
||||
securityKeyName: "密钥名称"
|
||||
lastUsed: "最后使用:"
|
||||
unregister: "删除账户"
|
||||
resetPassword: "重置密码"
|
||||
newPasswordIs: "新的密码是「{password}」"
|
||||
post: "投稿"
|
||||
posted: "已投稿"
|
||||
autoReloadWhenDisconnected: "断开连接时自动重新加载"
|
||||
reduceUiAnimation: "减少UI动画"
|
||||
share: "分享"
|
||||
notFound: "未找到"
|
||||
uploadFolder: "默认上传文件夹"
|
||||
cacheClear: "清空缓存"
|
||||
markAsReadAllNotifications: "将所有通知标为已读"
|
||||
markAsReadAllUnreadNotes: "将所有帖子标记为已读"
|
||||
markAsReadAllTalkMessages: "将所有对话标为已读"
|
||||
help: "帮助"
|
||||
inputMessageHere: "在此键入信息"
|
||||
close: "关闭"
|
||||
group: "群组"
|
||||
groups: "群组"
|
||||
createGroup: "创建群组"
|
||||
ownedGroups: "拥有的群组"
|
||||
joinedGroups: "已加入的群组"
|
||||
invites: "邀请"
|
||||
groupName: "群组名"
|
||||
members: "成员"
|
||||
transfer: "转让"
|
||||
_2fa:
|
||||
alreadyRegistered: "此设备已被注册"
|
||||
registerDevice: "注册设备"
|
||||
registerKey: "注册密钥"
|
||||
_permissions:
|
||||
"read:account": "查看账户信息"
|
||||
"write:account": "更改帐户信息"
|
||||
"read:blocks": "查看黑名单"
|
||||
"write:blocks": "编辑黑名单"
|
||||
"read:drive": "查看网盘"
|
||||
@ -223,16 +332,55 @@ _permissions:
|
||||
"write:user-groups": "操作用户组"
|
||||
_auth:
|
||||
permissionAsk: "这个应用程序需要以下权限"
|
||||
_weekday:
|
||||
sunday: "星期日"
|
||||
monday: "星期一"
|
||||
tuesday: "星期二"
|
||||
wednesday: "星期三"
|
||||
thursday: "星期四"
|
||||
friday: "星期五"
|
||||
saturday: "星期六"
|
||||
_widgets:
|
||||
memo: "便签"
|
||||
notifications: "通知"
|
||||
timeline: "时间线"
|
||||
calendar: "日历"
|
||||
trends: "趋势"
|
||||
clock: "时钟"
|
||||
rss: "RSS阅读器"
|
||||
_cw:
|
||||
hide: "隐藏"
|
||||
show: "查看更多"
|
||||
chars: "{count}个字符"
|
||||
files: "{count} 个文件"
|
||||
poll: "投票"
|
||||
_poll:
|
||||
noOnlyOneChoice: "需要至少两个选项"
|
||||
choiceN: "选择{n}"
|
||||
noMore: "无法再添加更多了"
|
||||
canMultipleVote: "允许多个投票"
|
||||
expiration: "截止时间"
|
||||
infinite: "无限期"
|
||||
at: "指定日期"
|
||||
after: "指定时间"
|
||||
deadlineDate: "截止日期"
|
||||
deadlineTime: "小时"
|
||||
duration: "时长"
|
||||
votesCount: "{n}票"
|
||||
totalVotes: "总票数{n}"
|
||||
vote: "投票"
|
||||
showResult: "显示结果"
|
||||
voted: "已投票"
|
||||
closed: "已截止"
|
||||
remainingDays: "{d}天{h}小时后截止"
|
||||
remainingHours: "{h}小时{m}分后截止"
|
||||
remainingMinutes: "{m}分{s}秒后截止"
|
||||
remainingSeconds: "{s}秒后截止"
|
||||
_visibility:
|
||||
public: "公开"
|
||||
home: "首页"
|
||||
followers: "关注者"
|
||||
specified: "指定用户"
|
||||
_profile:
|
||||
name: "名称"
|
||||
username: "用户名"
|
||||
@ -241,25 +389,261 @@ _exportOrImport:
|
||||
muteList: "屏蔽"
|
||||
blockingList: "屏蔽"
|
||||
userLists: "列表"
|
||||
_charts:
|
||||
usersIncDec: "用户数量:增加/减少"
|
||||
_instanceCharts:
|
||||
users: "用户数量:增加/减少"
|
||||
usersTotal: "用户总数"
|
||||
ff: "关注/被关注:数量变化"
|
||||
ffTotal: "关注/被关注:总数"
|
||||
cacheSize: "缓存大小:增加/减少"
|
||||
_timelines:
|
||||
home: "首页"
|
||||
local: "本地"
|
||||
social: "社交"
|
||||
global: "全局"
|
||||
_pages:
|
||||
newPage: "创建页面"
|
||||
editPage: "编辑页面"
|
||||
page-created: "页面已创建"
|
||||
page-updated: "页面已更新"
|
||||
name-already-exists: "该页面URL已存在"
|
||||
title-invalid-name: "无效的页面URL"
|
||||
text-invalid-name: "请确认该项不为空"
|
||||
editThisPage: "编辑此页面"
|
||||
viewSource: "查看源代码"
|
||||
viewPage: "查看页面"
|
||||
like: "赞"
|
||||
unlike: "取消赞"
|
||||
liked-pages: "喜欢的页面"
|
||||
my-pages: "我的页面"
|
||||
inspector: "检查器"
|
||||
content: "页面内容"
|
||||
variables: "变量"
|
||||
variables-info: "您可以使用变量创建动态页面。在文本中通过<b>{变量名}</b>的写法来嵌入变量值。例如在文本<b>Hello { thing } world!</b>中,如果变量(thing)的值为<b>ai</b>,那么该文本会成为<b>Hello ai world!</b>。"
|
||||
variables-info2: "因为变量的计算(计算变量值)是从上到下执行的,所以不能在变量中引用下面的变量。例如从上到下依次定义了<b>A,B,C</b>3个变量,那么<b>C</b>中可以引用<b>A</b>或<b>B</b>,但是<b>A</b>无法引用<b>B</b>或<b>C</b>。"
|
||||
variables-info3: "为了接收来自用户的输入,页面上设有“用户输入”块,在“变量名称”中设置要在其中保存输入值的变量名(变量会自动创建)。您可以使用该变量执行操作以响应用户输入。"
|
||||
variables-info4: "通过使用函数,您可以将数值计算过程组合成可重用的形式。要创建函数,需要创建一个“函数”类型的变量。你可以将函数设定为槽函数(参数)的格式,槽函数的值可作为函数中的变量使用。另外,AiScript标准中还有一些函数会将函数作为参数(称为高阶函数)。\n除了已经预先定义的函数外,您也可以将它们设置为这些高阶函数的槽函数。"
|
||||
more-details: "详细说明"
|
||||
title: "标题"
|
||||
url: "页面URL"
|
||||
summary: "页面摘要"
|
||||
alignCenter: "居中"
|
||||
hide-title-when-pinned: "置顶时隐藏标题"
|
||||
font: "字体"
|
||||
fontSerif: "衬线字体"
|
||||
fontSansSerif: "无衬线字体"
|
||||
set-eye-catching-image: "设置封面图片"
|
||||
remove-eye-catching-image: "删除封面图片"
|
||||
chooseBlock: "添加块"
|
||||
selectType: "选择类型"
|
||||
enterVariableName: "请输入变量名"
|
||||
the-variable-name-is-already-used: "变量名已使用"
|
||||
content-blocks: "内容"
|
||||
input-blocks: "输入"
|
||||
special-blocks: "特殊"
|
||||
post-from-post-form: "发布此内容"
|
||||
posted-from-post-form: "已发布"
|
||||
blocks:
|
||||
text: "文本"
|
||||
image: "图片"
|
||||
_if:
|
||||
variable: "变量"
|
||||
post: "投稿窗口"
|
||||
_textInput:
|
||||
name: "变量名"
|
||||
text: "标题"
|
||||
default: "默认值"
|
||||
textareaInput: "多行文本输入"
|
||||
_textareaInput:
|
||||
name: "变量名"
|
||||
text: "标题"
|
||||
default: "默认值"
|
||||
numberInput: "输入数值"
|
||||
_numberInput:
|
||||
name: "变量名"
|
||||
text: "标题"
|
||||
default: "默认值"
|
||||
switch: "开关"
|
||||
_switch:
|
||||
name: "变量名"
|
||||
text: "标题"
|
||||
default: "默认值"
|
||||
counter: "计数器"
|
||||
_counter:
|
||||
name: "变量名"
|
||||
text: "标题"
|
||||
inc: "增加值"
|
||||
_button:
|
||||
text: "标题"
|
||||
colored: "彩色"
|
||||
action: "按下按钮时的行为"
|
||||
_action:
|
||||
dialog: "显示对话框"
|
||||
_dialog:
|
||||
content: "内容"
|
||||
_radioButton:
|
||||
name: "变量名"
|
||||
title: "标题"
|
||||
default: "默认值"
|
||||
script:
|
||||
categories:
|
||||
list: "列表"
|
||||
blocks:
|
||||
text: "文本"
|
||||
multiLineText: "文本 (多行)"
|
||||
textList: "文本列表"
|
||||
_textList:
|
||||
info: "请使用换行符分隔每行"
|
||||
strLen: "文本长度"
|
||||
_strLen:
|
||||
arg1: "文本"
|
||||
strPick: "提取字符"
|
||||
_strPick:
|
||||
arg1: "文本"
|
||||
arg2: "字符位置"
|
||||
strReplace: "替换文本"
|
||||
_strReplace:
|
||||
arg1: "文本"
|
||||
arg2: "替换之前"
|
||||
arg3: "替换之后"
|
||||
strReverse: "文本反向"
|
||||
_strReverse:
|
||||
arg1: "文本"
|
||||
join: "合并文本"
|
||||
_join:
|
||||
arg1: "列表"
|
||||
arg2: "分隔符"
|
||||
add: "加"
|
||||
_add:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
subtract: "减"
|
||||
_subtract:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
multiply: "乘"
|
||||
_multiply:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
divide: "除"
|
||||
_divide:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
mod: "取模(MOD)"
|
||||
_mod:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
round: "四舍五入"
|
||||
_round:
|
||||
arg1: "数值"
|
||||
eq: "A和B相等"
|
||||
_eq:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
notEq: "A和B不等"
|
||||
_notEq:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
and: "A和B"
|
||||
_and:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
or: "A或B"
|
||||
_or:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
lt: "< A小于B"
|
||||
_lt:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
gt: "> A大于B"
|
||||
_gt:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
ltEq: "<= A小于等于B"
|
||||
_ltEq:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
gtEq: ">= A大于等于B"
|
||||
_gtEq:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
if: "分支"
|
||||
_if:
|
||||
arg1: "如果"
|
||||
arg2: "如果"
|
||||
arg3: "否则"
|
||||
not: "否"
|
||||
_not:
|
||||
arg1: "否"
|
||||
random: "随机"
|
||||
_random:
|
||||
arg1: "概率"
|
||||
rannum: "随机数"
|
||||
_rannum:
|
||||
arg1: "最小值"
|
||||
arg2: "最大值"
|
||||
randomPick: "从列表中随机选择"
|
||||
_randomPick:
|
||||
arg1: "列表"
|
||||
dailyRandom: "随机(每个用户每日)"
|
||||
_dailyRandom:
|
||||
arg1: "概率"
|
||||
dailyRannum: "随机数(每个用户每日)"
|
||||
_dailyRannum:
|
||||
arg1: "最小值"
|
||||
arg2: "最大值"
|
||||
dailyRandomPick: "从列表中随机选择(每个用户每日)"
|
||||
_dailyRandomPick:
|
||||
arg1: "列表"
|
||||
seedRandom: "随机 (种子)"
|
||||
_seedRandom:
|
||||
arg1: "种子"
|
||||
arg2: "概率"
|
||||
seedRannum: "随机数(种子)"
|
||||
_seedRannum:
|
||||
arg1: "种子"
|
||||
arg2: "最小值"
|
||||
arg3: "最大值"
|
||||
seedRandomPick: "从列表中随机选择 (种子)"
|
||||
_seedRandomPick:
|
||||
arg1: "种子"
|
||||
arg2: "列表"
|
||||
DRPWPM: "从概率列表中随机选择(每用户每天)"
|
||||
_DRPWPM:
|
||||
arg1: "文本列表"
|
||||
pick: "从列表中选择"
|
||||
_pick:
|
||||
arg1: "列表"
|
||||
arg2: "位置"
|
||||
listLen: "获取列表长度"
|
||||
_listLen:
|
||||
arg1: "列表"
|
||||
number: "数值"
|
||||
stringToNumber: "文本到数字"
|
||||
_stringToNumber:
|
||||
arg1: "文本"
|
||||
numberToString: "数字到文本"
|
||||
_numberToString:
|
||||
arg1: "数值"
|
||||
splitStrByLine: "将文本按行拆分"
|
||||
_splitStrByLine:
|
||||
arg1: "文本"
|
||||
ref: "变量"
|
||||
fn: "函数"
|
||||
_fn:
|
||||
arg1: "输出"
|
||||
for: "重复"
|
||||
_for:
|
||||
arg1: "次数"
|
||||
arg2: "处理"
|
||||
types:
|
||||
string: "文字"
|
||||
number: "数值"
|
||||
boolean: "Flag"
|
||||
array: "列表"
|
||||
stringArray: "文本列表"
|
||||
enviromentVariables: "环境变量"
|
||||
pageVariables: "页面元素"
|
||||
argVariables: "输入变量"
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <syuilotan@yahoo.co.jp>",
|
||||
"version": "12.3.0",
|
||||
"version": "12.5.0",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -193,6 +193,7 @@ export default Vue.extend({
|
||||
return {
|
||||
'p': this.post,
|
||||
'n': this.post,
|
||||
's': this.search,
|
||||
'h|/': this.help
|
||||
};
|
||||
},
|
||||
@ -206,6 +207,7 @@ export default Vue.extend({
|
||||
$route(to, from) {
|
||||
this.pageKey++;
|
||||
this.notificationsOpen = false;
|
||||
this.showNav = false;
|
||||
this.canBack = (window.history.length > 0 && !['index'].includes(to.name));
|
||||
},
|
||||
|
||||
@ -696,6 +698,8 @@ export default Vue.extend({
|
||||
> .sub {
|
||||
$post-button-size: 42px;
|
||||
$post-button-margin: (($header-height - $post-button-size) / 2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 16px;
|
||||
@ -735,7 +739,7 @@ export default Vue.extend({
|
||||
> .post {
|
||||
width: $post-button-size;
|
||||
height: $post-button-size;
|
||||
margin: $post-button-margin 0 $post-button-margin $post-button-margin;
|
||||
margin-left: $post-button-margin;
|
||||
border-radius: 100%;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="user in us" :key="user.id" style="width:32px;height:32px;margin-right:8px;">
|
||||
<div v-for="user in us" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
|
||||
<mk-avatar :user="user" style="width:32px;height:32px;"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -45,6 +45,8 @@ export default Vue.extend({
|
||||
height: 150px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<x-popup :source="source" :no-center="noCenter" :fixed="fixed" :width="width" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }">
|
||||
<sequential-entrance class="rrevdjwt" :class="{ left: align === 'left' }" :delay="15" :direction="direction">
|
||||
<x-popup :source="source" :no-center="noCenter" :fixed="fixed" :width="width" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap">
|
||||
<sequential-entrance class="rrevdjwt" :class="{ left: align === 'left' }" :delay="15" :direction="direction" ref="items">
|
||||
<template v-for="(item, i) in items.filter(item => item !== undefined)">
|
||||
<div v-if="item === null" class="divider" :key="i"></div>
|
||||
<span v-else-if="item.type === 'label'" class="label item" :key="i">
|
||||
@ -36,6 +36,7 @@
|
||||
import Vue from 'vue';
|
||||
import { faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import XPopup from './popup.vue';
|
||||
import { focusPrev, focusNext } from '../scripts/focus';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
@ -69,12 +70,31 @@ export default Vue.extend({
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
viaKeyboard: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
faCircle
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
'up|k|shift+tab': this.focusUp,
|
||||
'down|j|tab': this.focusDown,
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.viaKeyboard) {
|
||||
this.$nextTick(() => {
|
||||
focusNext(this.$refs.items.$slots.default[0].elm, true);
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clicked(fn) {
|
||||
fn();
|
||||
@ -82,6 +102,12 @@ export default Vue.extend({
|
||||
},
|
||||
close() {
|
||||
this.$refs.popup.close();
|
||||
},
|
||||
focusUp() {
|
||||
focusPrev(document.activeElement);
|
||||
},
|
||||
focusDown() {
|
||||
focusNext(document.activeElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -119,6 +145,10 @@ export default Vue.extend({
|
||||
background: var(--accentDarken);
|
||||
}
|
||||
|
||||
&:not(:active):focus {
|
||||
box-shadow: 0 0 0 2px var(--focus) inset;
|
||||
}
|
||||
|
||||
&.label {
|
||||
pointer-events: none;
|
||||
font-size: 0.7em;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }" v-once/>
|
||||
<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -1,200 +0,0 @@
|
||||
<template>
|
||||
<x-menu :source="source" :items="items" @closed="closed"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faStar, faLink, faThumbtack, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCopy, faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
|
||||
import i18n from '../i18n';
|
||||
import { url } from '../config';
|
||||
import copyToClipboard from '../scripts/copy-to-clipboard';
|
||||
import XMenu from './menu.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
components: {
|
||||
XMenu
|
||||
},
|
||||
props: ['note', 'source'],
|
||||
data() {
|
||||
return {
|
||||
isFavorited: false,
|
||||
isWatching: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
items(): any[] {
|
||||
if (this.$store.getters.isSignedIn) {
|
||||
return [{
|
||||
icon: faCopy,
|
||||
text: this.$t('copyContent'),
|
||||
action: this.copyContent
|
||||
}, {
|
||||
icon: faLink,
|
||||
text: this.$t('copyLink'),
|
||||
action: this.copyLink
|
||||
}, this.note.uri ? {
|
||||
icon: faExternalLinkSquareAlt,
|
||||
text: this.$t('showOnRemote'),
|
||||
action: () => {
|
||||
window.open(this.note.uri, '_blank');
|
||||
}
|
||||
} : undefined,
|
||||
null,
|
||||
this.isFavorited ? {
|
||||
icon: faStar,
|
||||
text: this.$t('unfavorite'),
|
||||
action: () => this.toggleFavorite(false)
|
||||
} : {
|
||||
icon: faStar,
|
||||
text: this.$t('favorite'),
|
||||
action: () => this.toggleFavorite(true)
|
||||
},
|
||||
this.note.userId != this.$store.state.i.id ? this.isWatching ? {
|
||||
icon: faEyeSlash,
|
||||
text: this.$t('unwatch'),
|
||||
action: () => this.toggleWatch(false)
|
||||
} : {
|
||||
icon: faEye,
|
||||
text: this.$t('watch'),
|
||||
action: () => this.toggleWatch(true)
|
||||
} : undefined,
|
||||
this.note.userId == this.$store.state.i.id ? (this.$store.state.i.pinnedNoteIds || []).includes(this.note.id) ? {
|
||||
icon: faThumbtack,
|
||||
text: this.$t('unpin'),
|
||||
action: () => this.togglePin(false)
|
||||
} : {
|
||||
icon: faThumbtack,
|
||||
text: this.$t('pin'),
|
||||
action: () => this.togglePin(true)
|
||||
} : undefined,
|
||||
...(this.note.userId == this.$store.state.i.id ? [
|
||||
null,
|
||||
{
|
||||
icon: faTrashAlt,
|
||||
text: this.$t('delete'),
|
||||
action: this.del
|
||||
}]
|
||||
: []
|
||||
)]
|
||||
.filter(x => x !== undefined);
|
||||
} else {
|
||||
return [{
|
||||
icon: faCopy,
|
||||
text: this.$t('copyContent'),
|
||||
action: this.copyContent
|
||||
}, {
|
||||
icon: faLink,
|
||||
text: this.$t('copyLink'),
|
||||
action: this.copyLink
|
||||
}, this.note.uri ? {
|
||||
icon: faExternalLinkSquareAlt,
|
||||
text: this.$t('showOnRemote'),
|
||||
action: () => {
|
||||
window.open(this.note.uri, '_blank');
|
||||
}
|
||||
} : undefined]
|
||||
.filter(x => x !== undefined);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$root.api('notes/state', {
|
||||
noteId: this.note.id
|
||||
}).then(state => {
|
||||
this.isFavorited = state.isFavorited;
|
||||
this.isWatching = state.isWatching;
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
copyContent() {
|
||||
copyToClipboard(this.note.text);
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
},
|
||||
|
||||
copyLink() {
|
||||
copyToClipboard(`${url}/notes/${this.note.id}`);
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
},
|
||||
|
||||
togglePin(pin: boolean) {
|
||||
this.$root.api(pin ? 'i/pin' : 'i/unpin', {
|
||||
noteId: this.note.id
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
this.$emit('closed');
|
||||
this.destroyDom();
|
||||
}).catch(e => {
|
||||
if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('pinLimitExceeded')
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
del() {
|
||||
this.$root.dialog({
|
||||
type: 'warning',
|
||||
text: this.$t('noteDeleteConfirm'),
|
||||
showCancelButton: true
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
|
||||
this.$root.api('notes/delete', {
|
||||
noteId: this.note.id
|
||||
}).then(() => {
|
||||
this.$emit('closed');
|
||||
this.destroyDom();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
toggleFavorite(favorite: boolean) {
|
||||
this.$root.api(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
|
||||
noteId: this.note.id
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
this.$emit('closed');
|
||||
this.destroyDom();
|
||||
});
|
||||
},
|
||||
|
||||
toggleWatch(watch: boolean) {
|
||||
this.$root.api(watch ? 'notes/watching/create' : 'notes/watching/delete', {
|
||||
noteId: this.note.id
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
this.$emit('closed');
|
||||
this.destroyDom();
|
||||
});
|
||||
},
|
||||
|
||||
closed() {
|
||||
this.$emit('closed');
|
||||
this.$nextTick(() => {
|
||||
this.destroyDom();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -19,7 +19,7 @@
|
||||
</router-link>
|
||||
</i18n>
|
||||
<div class="info">
|
||||
<button class="_button time" @click="showRenoteMenu"><mk-time :time="note.createdAt"/></button>
|
||||
<button class="_button time" @click="showRenoteMenu()" ref="renoteTime"><mk-time :time="note.createdAt"/></button>
|
||||
<span class="visibility" v-if="note.visibility != 'public'">
|
||||
<fa v-if="note.visibility == 'home'" :icon="faHome"/>
|
||||
<fa v-if="note.visibility == 'followers'" :icon="faUnlock"/>
|
||||
@ -83,7 +83,8 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faTrashAlt, faQuoteRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCopy, faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
|
||||
import { parse } from '../../mfm/parse';
|
||||
import { sum, unique } from '../../prelude/array';
|
||||
import i18n from '../i18n';
|
||||
@ -95,20 +96,11 @@ import XMediaList from './media-list.vue';
|
||||
import XCwButton from './cw-button.vue';
|
||||
import XPoll from './poll.vue';
|
||||
import XUrlPreview from './url-preview.vue';
|
||||
import MkNoteMenu from './note-menu.vue';
|
||||
import MkReactionPicker from './reaction-picker.vue';
|
||||
import pleaseLogin from '../scripts/please-login';
|
||||
|
||||
function focus(el, fn) {
|
||||
const target = fn(el);
|
||||
if (target) {
|
||||
if (target.hasAttribute('tabindex')) {
|
||||
target.focus();
|
||||
} else {
|
||||
focus(target, fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
import { focusPrev, focusNext } from '../scripts/focus';
|
||||
import { url } from '../config';
|
||||
import copyToClipboard from '../scripts/copy-to-clipboard';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
@ -148,7 +140,6 @@ export default Vue.extend({
|
||||
replies: [],
|
||||
showContent: false,
|
||||
hideThisNote: false,
|
||||
openingMenu: false,
|
||||
faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan
|
||||
};
|
||||
},
|
||||
@ -274,14 +265,8 @@ export default Vue.extend({
|
||||
methods: {
|
||||
capture(withHandler = false) {
|
||||
if (this.$store.getters.isSignedIn) {
|
||||
if (document.body.contains(this.$el)) {
|
||||
this.connection.send('sn', { id: this.appearNote.id });
|
||||
if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
|
||||
} else {
|
||||
this.$once('hook:activated', () => {
|
||||
this.capture(withHandler);
|
||||
});
|
||||
}
|
||||
this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
|
||||
if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
|
||||
}
|
||||
},
|
||||
|
||||
@ -375,7 +360,7 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
renote() {
|
||||
renote(viaKeyboard = false) {
|
||||
pleaseLogin(this.$root);
|
||||
this.blur();
|
||||
this.$root.menu({
|
||||
@ -397,6 +382,7 @@ export default Vue.extend({
|
||||
}
|
||||
}]
|
||||
source: this.$refs.renoteButton,
|
||||
viaKeyboard
|
||||
}).then(this.focus);
|
||||
},
|
||||
|
||||
@ -465,20 +451,114 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
menu(viaKeyboard = false) {
|
||||
if (this.openingMenu) return;
|
||||
this.openingMenu = true;
|
||||
const w = this.$root.new(MkNoteMenu, {
|
||||
source: this.$refs.menuButton,
|
||||
note: this.appearNote,
|
||||
animation: !viaKeyboard
|
||||
}).$once('closed', () => {
|
||||
this.openingMenu = false;
|
||||
this.focus();
|
||||
toggleFavorite(favorite: boolean) {
|
||||
this.$root.api(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
|
||||
noteId: this.appearNote.id
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
showRenoteMenu(ev) {
|
||||
toggleWatch(watch: boolean) {
|
||||
this.$root.api(watch ? 'notes/watching/create' : 'notes/watching/delete', {
|
||||
noteId: this.appearNote.id
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async menu(viaKeyboard = false) {
|
||||
let menu;
|
||||
if (this.$store.getters.isSignedIn) {
|
||||
const state = await this.$root.api('notes/state', {
|
||||
noteId: this.appearNote.id
|
||||
});
|
||||
menu = [{
|
||||
icon: faCopy,
|
||||
text: this.$t('copyContent'),
|
||||
action: this.copyContent
|
||||
}, {
|
||||
icon: faLink,
|
||||
text: this.$t('copyLink'),
|
||||
action: this.copyLink
|
||||
}, this.appearNote.uri ? {
|
||||
icon: faExternalLinkSquareAlt,
|
||||
text: this.$t('showOnRemote'),
|
||||
action: () => {
|
||||
window.open(this.appearNote.uri, '_blank');
|
||||
}
|
||||
} : undefined,
|
||||
null,
|
||||
state.isFavorited ? {
|
||||
icon: faStar,
|
||||
text: this.$t('unfavorite'),
|
||||
action: () => this.toggleFavorite(false)
|
||||
} : {
|
||||
icon: faStar,
|
||||
text: this.$t('favorite'),
|
||||
action: () => this.toggleFavorite(true)
|
||||
},
|
||||
this.appearNote.userId != this.$store.state.i.id ? state.isWatching ? {
|
||||
icon: faEyeSlash,
|
||||
text: this.$t('unwatch'),
|
||||
action: () => this.toggleWatch(false)
|
||||
} : {
|
||||
icon: faEye,
|
||||
text: this.$t('watch'),
|
||||
action: () => this.toggleWatch(true)
|
||||
} : undefined,
|
||||
this.appearNote.userId == this.$store.state.i.id ? (this.$store.state.i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
|
||||
icon: faThumbtack,
|
||||
text: this.$t('unpin'),
|
||||
action: () => this.togglePin(false)
|
||||
} : {
|
||||
icon: faThumbtack,
|
||||
text: this.$t('pin'),
|
||||
action: () => this.togglePin(true)
|
||||
} : undefined,
|
||||
...(this.appearNote.userId == this.$store.state.i.id ? [
|
||||
null,
|
||||
{
|
||||
icon: faTrashAlt,
|
||||
text: this.$t('delete'),
|
||||
action: this.del
|
||||
}]
|
||||
: []
|
||||
)]
|
||||
.filter(x => x !== undefined);
|
||||
} else {
|
||||
menu = [{
|
||||
icon: faCopy,
|
||||
text: this.$t('copyContent'),
|
||||
action: this.copyContent
|
||||
}, {
|
||||
icon: faLink,
|
||||
text: this.$t('copyLink'),
|
||||
action: this.copyLink
|
||||
}, this.appearNote.uri ? {
|
||||
icon: faExternalLinkSquareAlt,
|
||||
text: this.$t('showOnRemote'),
|
||||
action: () => {
|
||||
window.open(this.appearNote.uri, '_blank');
|
||||
}
|
||||
} : undefined]
|
||||
.filter(x => x !== undefined);
|
||||
}
|
||||
|
||||
this.$root.menu({
|
||||
items: menu,
|
||||
source: this.$refs.menuButton,
|
||||
viaKeyboard
|
||||
}).then(this.focus);
|
||||
},
|
||||
|
||||
showRenoteMenu(viaKeyboard = false) {
|
||||
if (!this.$store.getters.isSignedIn || (this.$store.state.i.id !== this.note.userId)) return;
|
||||
this.$root.menu({
|
||||
items: [{
|
||||
@ -491,7 +571,8 @@ export default Vue.extend({
|
||||
Vue.set(this.note, 'deletedAt', new Date());
|
||||
}
|
||||
}],
|
||||
source: ev.currentTarget || ev.target,
|
||||
source: this.$refs.renoteTime,
|
||||
viaKeyboard: viaKeyboard
|
||||
});
|
||||
},
|
||||
|
||||
@ -499,6 +580,40 @@ export default Vue.extend({
|
||||
this.showContent = !this.showContent;
|
||||
},
|
||||
|
||||
copyContent() {
|
||||
copyToClipboard(this.appearNote.text);
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
},
|
||||
|
||||
copyLink() {
|
||||
copyToClipboard(`${url}/notes/${this.appearNote.id}`);
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
},
|
||||
|
||||
togglePin(pin: boolean) {
|
||||
this.$root.api(pin ? 'i/pin' : 'i/unpin', {
|
||||
noteId: this.appearNote.id
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
}).catch(e => {
|
||||
if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('pinLimitExceeded')
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
focus() {
|
||||
this.$el.focus();
|
||||
},
|
||||
@ -508,11 +623,11 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
focusBefore() {
|
||||
focus(this.$el, e => e.previousElementSibling);
|
||||
focusPrev(this.$el);
|
||||
},
|
||||
|
||||
focusAfter() {
|
||||
focus(this.$el, e => e.nextElementSibling);
|
||||
focusNext(this.$el);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="mk-notes" v-size="[{ max: 500 }]">
|
||||
<div class="empty _panel" v-if="empty">
|
||||
<div class="empty" v-if="empty">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" alt=""/>
|
||||
<div>{{ $t('noNotes') }}</div>
|
||||
</div>
|
||||
|
||||
<mk-error v-if="error" @retry="init()"/>
|
||||
|
||||
<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note, i }">
|
||||
<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note }">
|
||||
<x-note :note="note" :detail="detail" :key="note.id"/>
|
||||
</x-list>
|
||||
|
||||
@ -94,6 +94,8 @@ export default Vue.extend({
|
||||
height: 128px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mk-popup">
|
||||
<div class="mk-popup" v-hotkey.global="keymap">
|
||||
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
|
||||
<div class="bg" ref="bg" @click="close()" v-if="show"></div>
|
||||
</transition>
|
||||
@ -35,6 +35,13 @@ export default Vue.extend({
|
||||
show: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
'esc': this.close,
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
const popover = this.$refs.content as any;
|
||||
@ -96,8 +103,8 @@ export default Vue.extend({
|
||||
methods: {
|
||||
close() {
|
||||
this.show = false;
|
||||
(this.$refs.bg as any).style.pointerEvents = 'none';
|
||||
(this.$refs.content as any).style.pointerEvents = 'none';
|
||||
if (this.$refs.bg) (this.$refs.bg as any).style.pointerEvents = 'none';
|
||||
if (this.$refs.content) (this.$refs.content as any).style.pointerEvents = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -153,7 +153,7 @@ export default Vue.extend({
|
||||
this.$t('_postForm._placeholders.f')
|
||||
];
|
||||
const x = xs[Math.floor(Math.random() * xs.length)];
|
||||
|
||||
|
||||
return this.renote
|
||||
? this.$t('_postForm.quotePlaceholder')
|
||||
: this.reply
|
||||
@ -217,7 +217,7 @@ export default Vue.extend({
|
||||
// デフォルト公開範囲
|
||||
this.applyVisibility(this.$store.state.settings.rememberNoteVisibility ? this.$store.state.device.visibility : this.$store.state.settings.defaultNoteVisibility);
|
||||
|
||||
this.localOnly = this.$store.state.settings.rememberNoteVisibility ? this.$store.state.device.localOnly : false;
|
||||
this.localOnly = this.$store.state.settings.rememberNoteVisibility ? this.$store.state.device.localOnly : this.$store.state.settings.defaultNoteLocalOnly;
|
||||
|
||||
// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
|
||||
if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
|
||||
@ -398,8 +398,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
applyVisibility(v: string) {
|
||||
if (!['public', 'home', 'followers', 'specified'].includes(v)) v = 'public'; // v11互換性のため
|
||||
this.visibility = v;
|
||||
this.visibility = ['public', 'home', 'followers', 'specified'].includes(v) ? v : 'public'; // v11互換性のため
|
||||
},
|
||||
|
||||
addVisibleUser() {
|
||||
@ -714,7 +713,7 @@ export default Vue.extend({
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: var(--fg);
|
||||
font-family: initial;
|
||||
font-family: inherit;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
padding: 0 16px;
|
||||
|
@ -27,6 +27,7 @@ export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
connection: null,
|
||||
connection2: null,
|
||||
pagination: null,
|
||||
baseQuery: {
|
||||
includeMyRenotes: this.$store.state.settings.showMyRenotes,
|
||||
@ -40,6 +41,7 @@ export default Vue.extend({
|
||||
created() {
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
this.connection.dispose();
|
||||
if (this.connection2) this.connection2.dispose();
|
||||
});
|
||||
|
||||
const prepend = note => {
|
||||
@ -54,6 +56,12 @@ export default Vue.extend({
|
||||
(this.$refs.tl as any).reload();
|
||||
};
|
||||
|
||||
const onChangeFollowing = () => {
|
||||
if (!this.$refs.tl.backed) {
|
||||
this.$refs.tl.reload();
|
||||
}
|
||||
};
|
||||
|
||||
let endpoint;
|
||||
|
||||
if (this.src == 'antenna') {
|
||||
@ -67,13 +75,12 @@ export default Vue.extend({
|
||||
this.connection.on('note', prepend);
|
||||
} else if (this.src == 'home') {
|
||||
endpoint = 'notes/timeline';
|
||||
const onChangeFollowing = () => {
|
||||
this.fetch();
|
||||
};
|
||||
this.connection = this.$root.stream.useSharedConnection('homeTimeline');
|
||||
this.connection.on('note', prepend);
|
||||
this.connection.on('follow', onChangeFollowing);
|
||||
this.connection.on('unfollow', onChangeFollowing);
|
||||
|
||||
this.connection2 = this.$root.stream.useSharedConnection('main');
|
||||
this.connection2.on('follow', onChangeFollowing);
|
||||
this.connection2.on('unfollow', onChangeFollowing);
|
||||
} else if (this.src == 'local') {
|
||||
endpoint = 'notes/local-timeline';
|
||||
this.connection = this.$root.stream.useSharedConnection('localTimeline');
|
||||
|
@ -254,7 +254,7 @@ export default Vue.extend({
|
||||
|
||||
> .input {
|
||||
position: relative;
|
||||
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
@ -327,14 +327,16 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
> input {
|
||||
$height: 32px;
|
||||
display: block;
|
||||
height: $height;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
line-height: 32px;
|
||||
line-height: $height;
|
||||
color: var(--inputText);
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
127
src/client/pages/index.home.tutorial.vue
Normal file
127
src/client/pages/index.home.tutorial.vue
Normal file
@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="_card tbkwesmv">
|
||||
<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('_tutorial.title') }}</div>
|
||||
<div class="_content" v-if="tutorial === 0">
|
||||
<div>{{ $t('_tutorial.step1_1') }}</div>
|
||||
<div>{{ $t('_tutorial.step1_2') }}</div>
|
||||
<div>{{ $t('_tutorial.step1_3') }}</div>
|
||||
</div>
|
||||
<div class="_content" v-else-if="tutorial === 1">
|
||||
<div>{{ $t('_tutorial.step2_1') }}</div>
|
||||
<div>{{ $t('_tutorial.step2_2') }}</div>
|
||||
<router-link class="_link" to="/my/settings">{{ $t('editProfile') }}</router-link>
|
||||
</div>
|
||||
<div class="_content" v-else-if="tutorial === 2">
|
||||
<div>{{ $t('_tutorial.step3_1') }}</div>
|
||||
<div>{{ $t('_tutorial.step3_2') }}</div>
|
||||
<div>{{ $t('_tutorial.step3_3') }}</div>
|
||||
<small>{{ $t('_tutorial.step3_4') }}</small>
|
||||
</div>
|
||||
<div class="_content" v-else-if="tutorial === 3">
|
||||
<div>{{ $t('_tutorial.step4_1') }}</div>
|
||||
<div>{{ $t('_tutorial.step4_2') }}</div>
|
||||
</div>
|
||||
<div class="_content" v-else-if="tutorial === 4">
|
||||
<div>{{ $t('_tutorial.step5_1') }}</div>
|
||||
<i18n path="_tutorial.step5_2" tag="div">
|
||||
<router-link class="_link" place="featured" to="/featured">{{ $t('featured') }}</router-link>
|
||||
<router-link class="_link" place="explore" to="/explore">{{ $t('explore') }}</router-link>
|
||||
</i18n>
|
||||
<div>{{ $t('_tutorial.step5_3') }}</div>
|
||||
<small>{{ $t('_tutorial.step5_4') }}</small>
|
||||
</div>
|
||||
<div class="_content" v-else-if="tutorial === 5">
|
||||
<div>{{ $t('_tutorial.step6_1') }}</div>
|
||||
<div>{{ $t('_tutorial.step6_2') }}</div>
|
||||
<div>{{ $t('_tutorial.step6_3') }}</div>
|
||||
</div>
|
||||
<div class="_content" v-else-if="tutorial === 6">
|
||||
<div>{{ $t('_tutorial.step7_1') }}</div>
|
||||
<i18n path="_tutorial.step7_2" tag="div">
|
||||
<router-link class="_link" place="help" to="/docs">{{ $t('help') }}</router-link>
|
||||
</i18n>
|
||||
<div>{{ $t('_tutorial.step7_3') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="_footer navigation">
|
||||
<div class="step">
|
||||
<button class="arrow _button" @click="tutorial--" :disabled="tutorial === 0">
|
||||
<fa :icon="faChevronLeft"/>
|
||||
</button>
|
||||
<span>{{ tutorial + 1 }} / 7</span>
|
||||
<button class="arrow _button" @click="tutorial++" :disabled="tutorial === 6">
|
||||
<fa :icon="faChevronRight"/>
|
||||
</button>
|
||||
</div>
|
||||
<mk-button class="ok" @click="tutorial = -1" primary v-if="tutorial === 6"><fa :icon="faCheck"/> {{ $t('gotIt') }}</mk-button>
|
||||
<mk-button class="ok" @click="tutorial++" primary v-else><fa :icon="faCheck"/> {{ $t('next') }}</mk-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faInfoCircle, faChevronLeft, faChevronRight, faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||
import MkButton from '../components/ui/button.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
MkButton,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
faInfoCircle, faChevronLeft, faChevronRight, faCheck
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
tutorial: {
|
||||
get() { return this.$store.state.settings.tutorial || 0; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'tutorial', value }); }
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tbkwesmv {
|
||||
> ._content {
|
||||
> small {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
> .navigation {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
|
||||
> .step {
|
||||
> .arrow {
|
||||
padding: 4px;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
> .ok {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -13,6 +13,9 @@
|
||||
<fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/>
|
||||
</button>
|
||||
</portal>
|
||||
|
||||
<x-tutorial class="tutorial" v-if="$store.state.settings.tutorial != -1"/>
|
||||
|
||||
<x-timeline ref="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src" :src="src" :list="list" :antenna="antenna" @before="before()" @after="after()"/>
|
||||
</div>
|
||||
</template>
|
||||
@ -23,6 +26,7 @@ import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatell
|
||||
import { faComments } from '@fortawesome/free-regular-svg-icons';
|
||||
import Progress from '../scripts/loading';
|
||||
import XTimeline from '../components/timeline.vue';
|
||||
import XTutorial from './index.home.tutorial.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
metaInfo() {
|
||||
@ -32,7 +36,8 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
components: {
|
||||
XTimeline
|
||||
XTimeline,
|
||||
XTutorial,
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -57,7 +62,7 @@ export default Vue.extend({
|
||||
return {
|
||||
't': this.focus
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@ -172,7 +177,13 @@ export default Vue.extend({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.mk-home {
|
||||
> .tutorial {
|
||||
margin-bottom: var(--margin);
|
||||
}
|
||||
}
|
||||
|
||||
._kjvfvyph_ {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
@ -120,30 +120,30 @@
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
|
||||
<div class="_content">
|
||||
<header><fa :icon="faTwitter"/> {{ $t('twitter-integration-config') }}</header>
|
||||
<mk-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</mk-switch>
|
||||
<header><fa :icon="faTwitter"/> Twitter</header>
|
||||
<mk-switch v-model="enableTwitterIntegration">{{ $t('enable') }}</mk-switch>
|
||||
<template v-if="enableTwitterIntegration">
|
||||
<mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('twitter-integration-consumer-key') }}</mk-input>
|
||||
<mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('twitter-integration-consumer-secret') }}</mk-input>
|
||||
<mk-info>{{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }}</mk-info>
|
||||
<mk-info>Callback URL: {{ `${url}/api/tw/cb` }}</mk-info>
|
||||
<mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Key</mk-input>
|
||||
<mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Secret</mk-input>
|
||||
</template>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<header><fa :icon="faGithub"/> {{ $t('github-integration-config') }}</header>
|
||||
<mk-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</mk-switch>
|
||||
<header><fa :icon="faGithub"/> GitHub</header>
|
||||
<mk-switch v-model="enableGithubIntegration">{{ $t('enable') }}</mk-switch>
|
||||
<template v-if="enableGithubIntegration">
|
||||
<mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('github-integration-client-id') }}</mk-input>
|
||||
<mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('github-integration-client-secret') }}</mk-input>
|
||||
<mk-info>{{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }}</mk-info>
|
||||
<mk-info>Callback URL: {{ `${url}/api/gh/cb` }}</mk-info>
|
||||
<mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
|
||||
<mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
|
||||
</template>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<header><fa :icon="faDiscord"/> {{ $t('discord-integration-config') }}</header>
|
||||
<mk-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</mk-switch>
|
||||
<header><fa :icon="faDiscord"/> Discord</header>
|
||||
<mk-switch v-model="enableDiscordIntegration">{{ $t('enable') }}</mk-switch>
|
||||
<template v-if="enableDiscordIntegration">
|
||||
<mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('discord-integration-client-id') }}</mk-input>
|
||||
<mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('discord-integration-client-secret') }}</mk-input>
|
||||
<mk-info>{{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }}</mk-info>
|
||||
<mk-info>Callback URL: {{ `${url}/api/dc/cb` }}</mk-info>
|
||||
<mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
|
||||
<mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
|
||||
</template>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
@ -180,7 +180,7 @@ import MkTextarea from '../../components/ui/textarea.vue';
|
||||
import MkSwitch from '../../components/ui/switch.vue';
|
||||
import MkInfo from '../../components/ui/info.vue';
|
||||
import MkUserSelect from '../../components/user-select.vue';
|
||||
import { version } from '../../config';
|
||||
import { version, url } from '../../config';
|
||||
import i18n from '../../i18n';
|
||||
import getAcct from '../../../misc/acct/render';
|
||||
|
||||
@ -204,6 +204,7 @@ export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
version,
|
||||
url,
|
||||
meta: null,
|
||||
stats: null,
|
||||
serverInfo: null,
|
||||
|
@ -247,6 +247,7 @@ export default Vue.extend({
|
||||
padding: 16px 16px 0 16px;
|
||||
resize: none;
|
||||
font-size: 1em;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
|
@ -8,6 +8,7 @@
|
||||
<portal to="avatar"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal>
|
||||
</template>
|
||||
<template v-if="!fetching && group">
|
||||
<portal to="icon"><fa :icon="faUsers"/></portal>
|
||||
<portal to="title">{{ group.name }}</portal>
|
||||
</template>
|
||||
|
||||
@ -35,7 +36,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faArrowCircleDown, faFlag } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faArrowCircleDown, faFlag, faUsers } from '@fortawesome/free-solid-svg-icons';
|
||||
import i18n from '../i18n';
|
||||
import XList from '../components/date-separated-list.vue';
|
||||
import XMessage from './messaging-room.message.vue';
|
||||
@ -63,7 +64,7 @@ export default Vue.extend({
|
||||
connection: null,
|
||||
showIndicator: false,
|
||||
timer: null,
|
||||
faArrowCircleDown, faFlag
|
||||
faArrowCircleDown, faFlag, faUsers
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -117,10 +117,12 @@ export default Vue.extend({
|
||||
start(ev) {
|
||||
this.$root.menu({
|
||||
items: [{
|
||||
text: this.$t('withUser'),
|
||||
text: this.$t('messagingWithUser'),
|
||||
icon: faUser,
|
||||
action: () => { this.startUser() }
|
||||
}, {
|
||||
text: this.$t('withGroup'),
|
||||
text: this.$t('messagingWithGroup'),
|
||||
icon: faUsers,
|
||||
action: () => { this.startGroup() }
|
||||
}],
|
||||
noCenter: true,
|
||||
@ -139,7 +141,7 @@ export default Vue.extend({
|
||||
const groups2 = await this.$root.api('users/groups/joined');
|
||||
const { canceled, result: group } = await this.$root.dialog({
|
||||
type: null,
|
||||
title: this.$t('select-group'),
|
||||
title: this.$t('group'),
|
||||
select: {
|
||||
items: groups1.concat(groups2).map(group => ({
|
||||
value: group, text: group.name
|
||||
@ -148,7 +150,7 @@ export default Vue.extend({
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
this.navigateGroup(group);
|
||||
this.$router.push(`/my/messaging/group/${group.id}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
<mk-container :body-togglable="true">
|
||||
<template #header><fa :icon="faEnvelopeOpenText"/> {{ $t('invites') }}</template>
|
||||
<mk-pagination :pagination="invitePagination" #default="{items}">
|
||||
<mk-pagination :pagination="invitePagination" #default="{items}" ref="invites">
|
||||
<div class="_frame" v-for="invite in items" :key="invite.id">
|
||||
<div class="_title">{{ invite.group.name }}</div>
|
||||
<div class="_content"><mk-avatars :user-ids="invite.group.userIds"/></div>
|
||||
@ -31,7 +31,7 @@
|
||||
|
||||
<mk-container :body-togglable="true">
|
||||
<template #header><fa :icon="faUsers"/> {{ $t('joinedGroups') }}</template>
|
||||
<mk-pagination :pagination="joinedPagination" #default="{items}">
|
||||
<mk-pagination :pagination="joinedPagination" #default="{items}" ref="joined">
|
||||
<div class="_frame" v-for="group in items" :key="group.id">
|
||||
<div class="_title">{{ group.name }}</div>
|
||||
<div class="_content"><mk-avatars :user-ids="group.userIds"/></div>
|
||||
@ -95,6 +95,25 @@ export default Vue.extend({
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
},
|
||||
acceptInvite(invite) {
|
||||
this.$root.api('users/groups/invitations/accept', {
|
||||
inviteId: invite.id
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
this.$refs.invites.reload();
|
||||
this.$refs.joined.reload();
|
||||
});
|
||||
},
|
||||
rejectInvite(invite) {
|
||||
this.$root.api('users/groups/invitations/reject', {
|
||||
inviteId: invite.id
|
||||
}).then(() => {
|
||||
this.$refs.invites.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -45,6 +45,8 @@ export default Vue.extend({
|
||||
height: 150px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
{{ $t('autoReloadWhenDisconnected') }}
|
||||
</mk-switch>
|
||||
<mk-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch">
|
||||
{{ $t('autoNoteWatch') }}<template #desc>{{ $t('auto-watch-desc') }}</template>
|
||||
{{ $t('autoNoteWatch') }}<template #desc>{{ $t('autoNoteWatchDescription') }}</template>
|
||||
</mk-switch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
|
@ -10,9 +10,11 @@
|
||||
<mk-select v-model="defaultNoteVisibility" style="margin-bottom: 8px;" v-if="!rememberNoteVisibility">
|
||||
<template #label>{{ $t('defaultNoteVisibility') }}</template>
|
||||
<option value="public">{{ $t('_visibility.public') }}</option>
|
||||
<option value="home">{{ $t('_visibility.home') }}</option>
|
||||
<option value="followers">{{ $t('_visibility.followers') }}</option>
|
||||
<option value="specified">{{ $t('_visibility.specified') }}</option>
|
||||
</mk-select>
|
||||
<mk-switch v-model="defaultNoteLocalOnly" v-if="!rememberNoteVisibility">{{ $t('_visibility.localOnly') }}</mk-switch>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@ -46,6 +48,11 @@ export default Vue.extend({
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
|
||||
},
|
||||
|
||||
defaultNoteLocalOnly: {
|
||||
get() { return this.$store.state.settings.defaultNoteLocalOnly; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteLocalOnly', value }); }
|
||||
},
|
||||
|
||||
rememberNoteVisibility: {
|
||||
get() { return this.$store.state.settings.rememberNoteVisibility; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); }
|
||||
|
@ -31,6 +31,7 @@ export const router = new VueRouter({
|
||||
{ path: '/my/mentions', component: page('mentions') },
|
||||
{ path: '/my/messaging', name: 'messaging', component: page('messaging') },
|
||||
{ path: '/my/messaging/:user', component: page('messaging-room') },
|
||||
{ path: '/my/messaging/group/:group', component: page('messaging-room') },
|
||||
{ path: '/my/drive', name: 'drive', component: page('drive') },
|
||||
{ path: '/my/drive/folder/:folder', component: page('drive') },
|
||||
{ path: '/my/pages', name: 'pages', component: page('pages') },
|
||||
|
23
src/client/scripts/focus.ts
Normal file
23
src/client/scripts/focus.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export function focusPrev(el: Element | null, self = false) {
|
||||
if (el == null) return;
|
||||
if (!self) el = el.previousElementSibling;
|
||||
if (el) {
|
||||
if (el.hasAttribute('tabindex')) {
|
||||
(el as HTMLElement).focus();
|
||||
} else {
|
||||
focusPrev(el.previousElementSibling, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function focusNext(el: Element | null, self = false) {
|
||||
if (el == null) return;
|
||||
if (!self) el = el.nextElementSibling;
|
||||
if (el) {
|
||||
if (el.hasAttribute('tabindex')) {
|
||||
(el as HTMLElement).focus();
|
||||
} else {
|
||||
focusPrev(el.nextElementSibling, true);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,8 @@ export default (opts) => ({
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
inited: false,
|
||||
more: false
|
||||
more: false,
|
||||
backed: false,
|
||||
};
|
||||
},
|
||||
|
||||
@ -78,6 +79,7 @@ export default (opts) => ({
|
||||
async fetchMore() {
|
||||
if (!this.more || this.moreFetching || this.items.length === 0) return;
|
||||
this.moreFetching = true;
|
||||
this.backed = true;
|
||||
let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params;
|
||||
if (params && params.then) params = await params;
|
||||
const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
|
||||
|
@ -5,10 +5,12 @@ import * as nestedProperty from 'nested-property';
|
||||
import MiOS from './mios';
|
||||
|
||||
const defaultSettings = {
|
||||
tutorial: 0,
|
||||
keepCw: false,
|
||||
showFullAcct: false,
|
||||
rememberNoteVisibility: false,
|
||||
defaultNoteVisibility: 'public',
|
||||
defaultNoteLocalOnly: false,
|
||||
uploadFolder: null,
|
||||
pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
|
||||
wallpaper: null,
|
||||
|
@ -18,7 +18,7 @@ self.addEventListener('install', ev => {
|
||||
caches.open(cacheName)
|
||||
.then(cache => {
|
||||
return cache.addAll([
|
||||
'/'
|
||||
`/?v=${version}`
|
||||
]);
|
||||
})
|
||||
.then(() => self.skipWaiting())
|
||||
@ -45,7 +45,7 @@ self.addEventListener('fetch', ev => {
|
||||
return response || fetch(ev.request);
|
||||
})
|
||||
.catch(() => {
|
||||
return caches.match('/');
|
||||
return caches.match(`/?v=${version}`);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -10,6 +10,7 @@
|
||||
<tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>新規投稿</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
|
||||
<tr><td><kbd class="key">T</kbd></td><td>タイムラインの最も新しい投稿にフォーカス</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
|
||||
<tr><td><kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">N</kbd></kbd></td><td>通知を表示/隠す</td><td><b>N</b>otifications</td></tr>
|
||||
<tr><td><kbd class="key">S</kbd></td><td>検索</td><td><b>S</b>earch</td></tr>
|
||||
<tr><td><kbd class="key">H</kbd>, <kbd class="key">?</kbd></td><td>ヘルプを表示</td><td><b>H</b>elp</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -3,6 +3,7 @@ import { IRemoteUser } from '../../../../models/entities/user';
|
||||
import createNote from './note';
|
||||
import { ICreate, getApId, validPost } from '../../type';
|
||||
import { apLogger } from '../../logger';
|
||||
import { toArray, concat, unique } from '../../../../prelude/array';
|
||||
|
||||
const logger = apLogger;
|
||||
|
||||
@ -11,6 +12,22 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
|
||||
|
||||
logger.info(`Create: ${uri}`);
|
||||
|
||||
// copy audiences between activity <=> object.
|
||||
if (typeof activity.object === 'object') {
|
||||
const to = unique(concat([toArray(activity.to), toArray(activity.object.to)]));
|
||||
const cc = unique(concat([toArray(activity.cc), toArray(activity.object.cc)]));
|
||||
|
||||
activity.to = to;
|
||||
activity.cc = cc;
|
||||
activity.object.to = to;
|
||||
activity.object.cc = cc;
|
||||
}
|
||||
|
||||
// If there is no attributedTo, use Activity actor.
|
||||
if (typeof activity.object === 'object' && !activity.object.attributedTo) {
|
||||
activity.object.attributedTo = activity.actor;
|
||||
}
|
||||
|
||||
const resolver = new Resolver();
|
||||
|
||||
const object = await resolver.resolve(activity.object).catch(e => {
|
||||
|
@ -15,7 +15,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, note: IObj
|
||||
try {
|
||||
const exist = await fetchNote(note);
|
||||
if (exist == null) {
|
||||
await createNote(note, resolver, silent, activity);
|
||||
await createNote(note, resolver, silent);
|
||||
}
|
||||
} finally {
|
||||
unlock();
|
||||
|
@ -17,7 +17,7 @@ import { deliverQuestionUpdate } from '../../../services/note/polls/update';
|
||||
import { extractDbHost, toPuny } from '../../../misc/convert-host';
|
||||
import { Notes, Emojis, Polls, MessagingMessages } from '../../../models';
|
||||
import { Note } from '../../../models/entities/note';
|
||||
import { IObject, getOneApId, getApId, validPost, ICreate, isCreate, IPost } from '../type';
|
||||
import { IObject, getOneApId, getApId, validPost, IPost } from '../type';
|
||||
import { Emoji } from '../../../models/entities/emoji';
|
||||
import { genId } from '../../../misc/gen-id';
|
||||
import { fetchMeta } from '../../../misc/fetch-meta';
|
||||
@ -78,7 +78,7 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P
|
||||
/**
|
||||
* Noteを作成します。
|
||||
*/
|
||||
export async function createNote(value: string | IObject, resolver?: Resolver, silent = false, activity?: ICreate): Promise<Note | null> {
|
||||
export async function createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
|
||||
if (resolver == null) resolver = new Resolver();
|
||||
|
||||
const object: any = await resolver.resolve(value);
|
||||
@ -112,18 +112,12 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
|
||||
|
||||
const noteAudience = await parseAudience(actor, note.to, note.cc);
|
||||
let visibility = noteAudience.visibility;
|
||||
let visibleUsers = noteAudience.visibleUsers;
|
||||
let apMentions = noteAudience.mentionedUsers;
|
||||
const visibleUsers = noteAudience.visibleUsers;
|
||||
const apMentions = noteAudience.mentionedUsers;
|
||||
|
||||
// Audience (to, cc) が指定されてなかった場合
|
||||
if (visibility === 'specified' && visibleUsers.length === 0) {
|
||||
if (activity && isCreate(activity)) {
|
||||
// Create 起因ならば Activity を見る
|
||||
const activityAudience = await parseAudience(actor, activity.to, activity.cc);
|
||||
visibility = activityAudience.visibility;
|
||||
visibleUsers = activityAudience.visibleUsers;
|
||||
apMentions = activityAudience.mentionedUsers;
|
||||
} else if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している
|
||||
if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している
|
||||
// こちらから匿名GET出来たものならばpublic
|
||||
visibility = 'public';
|
||||
}
|
||||
|
@ -13,6 +13,11 @@ export const meta = {
|
||||
default: 10
|
||||
},
|
||||
|
||||
withUnreads: {
|
||||
validator: $.optional.boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
@ -38,5 +43,5 @@ export default define(meta, async (ps, user) => {
|
||||
}
|
||||
}
|
||||
|
||||
return announcements;
|
||||
return ps.withUnreads ? announcements.filter((a: any) => !a.isRead) : announcements;
|
||||
});
|
||||
|
@ -60,8 +60,9 @@ export default class Connection {
|
||||
switch (type) {
|
||||
case 'api': this.onApiRequest(body); break;
|
||||
case 'readNotification': this.onReadNotification(body); break;
|
||||
case 'subNote': this.onSubscribeNote(body); break;
|
||||
case 'sn': this.onSubscribeNote(body); break; // alias
|
||||
case 'subNote': this.onSubscribeNote(body, true); break;
|
||||
case 'sn': this.onSubscribeNote(body, true); break; // alias
|
||||
case 's': this.onSubscribeNote(body, false); break;
|
||||
case 'unsubNote': this.onUnsubscribeNote(body); break;
|
||||
case 'un': this.onUnsubscribeNote(body); break; // alias
|
||||
case 'connect': this.onChannelConnectRequested(body); break;
|
||||
@ -107,7 +108,7 @@ export default class Connection {
|
||||
* 投稿購読要求時
|
||||
*/
|
||||
@autobind
|
||||
private onSubscribeNote(payload: any) {
|
||||
private onSubscribeNote(payload: any, read: boolean) {
|
||||
if (!payload.id) return;
|
||||
|
||||
if (this.subscribingNotes[payload.id] == null) {
|
||||
@ -120,7 +121,7 @@ export default class Connection {
|
||||
this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
|
||||
}
|
||||
|
||||
if (this.user) {
|
||||
if (this.user && read) {
|
||||
readNote(this.user.id, payload.id);
|
||||
}
|
||||
}
|
||||
|
@ -327,6 +327,10 @@ const override = (source: string, target: string, depth: number = 0) =>
|
||||
router.get('/othello', async ctx => ctx.redirect(override(ctx.URL.pathname, 'games/reversi', 1)));
|
||||
router.get('/reversi', async ctx => ctx.redirect(override(ctx.URL.pathname, 'games')));
|
||||
|
||||
router.get('/flush', async ctx => {
|
||||
await ctx.render('flush');
|
||||
});
|
||||
|
||||
// Render base html for all requests
|
||||
router.get('*', async ctx => {
|
||||
const meta = await fetchMeta();
|
||||
|
20
src/server/web/views/flush.pug
Normal file
20
src/server/web/views/flush.pug
Normal file
@ -0,0 +1,20 @@
|
||||
doctype html
|
||||
|
||||
html
|
||||
script.
|
||||
localStorage.removeItem('locale');
|
||||
|
||||
try {
|
||||
navigator.serviceWorker.controller.postMessage('clear');
|
||||
|
||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||
return Promise.all(registrations.map(registration => registration.unregister()));
|
||||
}).then(() => {
|
||||
location = '/';
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setTimeout(() => {
|
||||
location = '/';
|
||||
}, 10000)
|
||||
}
|
Reference in New Issue
Block a user