Merge branch 'develop' into beta

This commit is contained in:
ThatOneCalculator 2023-04-29 15:44:46 -07:00
commit b17bab4981
No known key found for this signature in database
GPG key ID: 8703CACD01000000
83 changed files with 1445 additions and 816 deletions

View file

@ -113,14 +113,9 @@ id: 'aid'
reservedUsernames:
- root
- admin
- administrator
- me
- system
- test
- proxy
- relay
- mod
- moderator
- info
- information
# Whether disable HSTS
#disableHsts: true
@ -152,6 +147,7 @@ reservedUsernames:
#proxy: http://127.0.0.1:3128
#proxyBypassHosts: [
# 'web.kaiteki.app',
# 'example.com',
# '192.0.2.8'
#]

View file

@ -27,9 +27,9 @@
- Notable differences:
- Improved UI/UX (especially on mobile)
- Improved notifications
- Fediverse account migration
- Improved instance security
- Improved accessibility
- Improved threads
- Recommended Instances timeline
- OCR image captioning
- New and improved Groups

View file

@ -1,7 +1,7 @@
_lang_: "Català"
headlineMisskey: "Una xarxa social de codi obert, descentralitzada i gratuita per\
\ sempre \U0001F680"
introMisskey: "Benvinguts! Calckey es una plataforma social de codi obert, descentralitzada\
introMisskey: "Benvinguts! Calckey es una plataforma social de codi obert, descentralitzada\
\ i gratuita per sempre! \U0001F680"
monthAndDay: "{day}/{month}"
search: "Cercar"
@ -15,43 +15,43 @@ gotIt: "Ho he entès!"
cancel: "Cancel·lar"
enterUsername: "Introdueix el teu nom d'usuari"
renotedBy: "Resignat per {user}"
noNotes: "Cap nota"
noNotes: "Cap publicació"
noNotifications: "Cap notificació"
instance: "Instàncies"
instance: "Instància"
settings: "Preferències"
basicSettings: "Configuració bàsica"
otherSettings: "Configuració avançada"
openInWindow: "Obrir en una nova finestra"
otherSettings: "Altres opcions"
openInWindow: "Obrir en una finestra nova"
profile: "Perfil"
timeline: "Línia de temps"
noAccountDescription: "Aquest usuari encara no ha escrit la seva biografia."
login: "Iniciar sessió"
loggingIn: "Identificant-se"
logout: "Tancar la sessió"
loggingIn: "Iniciant sessió"
logout: "Tancar sessió"
signup: "Registrar-se"
uploading: "Pujant..."
save: "Desar"
users: "Usuaris"
addUser: "Afegir un usuari"
favorite: "Afegir a preferits"
favorite: "Afegir a favorits"
favorites: "Favorits"
unfavorite: "Eliminar dels preferits"
favorited: "Afegit als preferits."
alreadyFavorited: "Ja s'ha afegit als preferits."
cantFavorite: "No s'ha pogut afegir als preferits."
unfavorite: "Eliminar de favorits"
favorited: "Afegit a favorits."
alreadyFavorited: "Ja s'ha afegit a favorits."
cantFavorite: "No s'ha pogut afegir a favorits."
pin: "Fixar al perfil"
unpin: "Para de fixar del perfil"
copyContent: "Copiar el contingut"
copyLink: "Copiar l'enllaç"
delete: "Eliminar"
deleteAndEdit: "Esborrar i editar"
deleteAndEditConfirm: "Estàs segur que vols suprimir aquesta nota i editar-la? Perdràs\
\ totes les reaccions, notes i respostes."
addToList: "Afegir a una llista"
unpin: "Deixar de fixar al perfil"
copyContent: "Còpia el contingut"
copyLink: "Còpia l'enllaç"
delete: "Esborra"
deleteAndEdit: "Esborrar i edita"
deleteAndEditConfirm: "Estàs segur que vols esborrar aquesta nota i editar-la? Perdràs\
\ totes les reaccions, resignats i respostes."
addToList: "Afegir a la llista"
sendMessage: "Enviar un missatge"
copyUsername: "Copiar nom d'usuari"
searchUser: "Cercar usuaris"
reply: "Respondre"
copyUsername: "Còpia nom d'usuari"
searchUser: "Cercar un usuari"
reply: "Respon"
loadMore: "Carregar més"
showMore: "Veure més"
youGotNewFollower: "t'ha seguit"
@ -60,21 +60,21 @@ followRequestAccepted: "Sol·licitud de seguiment acceptada"
mention: "Menció"
mentions: "Mencions"
directNotes: "Missatges directes"
importAndExport: "Importar / Exportar"
importAndExport: "Importar / Exportar Dades"
import: "Importar"
export: "Exportar"
files: "Fitxers"
download: "Baixar"
driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les notes\
\ associades a aquest fitxer adjunt també se suprimiran."
download: "Descarregar"
driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les publicacions\
\ associades a aquest fitxer adjunt també es suprimiran."
unfollowConfirm: "Estàs segur que vols deixar de seguir {name}?"
exportRequested: "Has sol·licitat una exportació. Això pot trigar una estona. S'afegirà\
\ a la teva unitat un cop completat."
\ al teu Disc un cop completada."
importRequested: "Has sol·licitat una importació. Això pot trigar una estona."
lists: "Llistes"
noLists: "No tens cap llista"
note: "Post"
notes: "Posts"
note: "Publicació"
notes: "Publicacions"
following: "Seguint"
followers: "Seguidors"
followsYou: "Et segueix"
@ -83,7 +83,7 @@ manageLists: "Gestionar les llistes"
error: "Error"
somethingHappened: "S'ha produït un error"
retry: "Torna-ho a intentar"
pageLoadError: "S'ha produït un error en carregar la pàgina"
pageLoadError: "Alguna cosa a sortit malament al carregar la pàgina."
pageLoadErrorDescription: "Això normalment es deu a errors de xarxa o a la memòria\
\ cau del navegador. Prova d'esborrar la memòria cau i torna-ho a provar després\
\ d'esperar una estona."
@ -100,13 +100,13 @@ followRequests: "Sol·licituds de seguiment"
unfollow: "Deixar de seguir"
followRequestPending: "Sol·licituds de seguiment pendents"
enterEmoji: "Introduir un emoji"
renote: "Renotar"
unrenote: "Anul·lar renota"
renoted: "Renotat."
cantRenote: "Aquesta publicació no pot ser renotada."
cantReRenote: "Impossible renotar una renota."
renote: "Impulsà"
unrenote: "Anul·lar impuls"
renoted: "Impulsat."
cantRenote: "Aquesta publicació no pot ser impulsada."
cantReRenote: "No es pot impulsar un impuls."
quote: "Citar"
pinnedNote: "Nota fixada"
pinnedNote: "Publicació fixada"
pinned: "Fixar al perfil"
you: "Tu"
clickToShow: "Fes clic per mostrar"
@ -116,7 +116,7 @@ reaction: "Reaccions"
reactionSetting: "Reaccions a mostrar al selector de reaccions"
reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem\
\ \"+\" per afegir."
rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes"
rememberNoteVisibility: "Recorda la configuració de visibilitat de les publicacions"
attachCancel: "Eliminar el fitxer adjunt"
markAsSensitive: "Marcar com a NSFW"
unmarkAsSensitive: "Deixar de marcar com a sensible"
@ -130,7 +130,7 @@ unsuspend: "Deixa de suspendre"
instances: "Instàncies"
remove: "Eliminar"
nsfw: "NSFW"
pinnedNotes: "Nota fixada"
pinnedNotes: "Publicació fixada"
userList: "Llistes"
smtpUser: "Nom d'usuari"
smtpPass: "Contrasenya"
@ -147,7 +147,7 @@ _mfm:
_theme:
keys:
mention: "Menció"
renote: "Renotar"
renote: "Impulsar"
_sfx:
note: "Posts"
notification: "Notificacions"
@ -191,12 +191,12 @@ _notification:
_types:
follow: "Seguint"
mention: "Menció"
renote: "Renotar"
renote: "Impulsos"
quote: "Citar"
reaction: "Reaccions"
_actions:
reply: "Respondre"
renote: "Renotar"
renote: "Impulsos"
_deck:
_columns:
notifications: "Notificacions"
@ -469,3 +469,209 @@ enableLocalTimeline: Activa la línea de temps local
enableRecommendedTimeline: Activa la línea de temps de recomanats
pinnedClipId: ID del clip que vols fixar
hcaptcha: hCaptcha
manageAntennas: Gestiona les Antenes
name: Nom
notesAndReplies: Articles i respostes
silence: Posa en silenci
withFiles: Amb fitxers
popularUsers: Usuaris populars
exploreUsersCount: Hi han {count} usuaris
exploreFediverse: Explora el Fesiverse
popularTags: Etiquetes populars
about: Sobre
recentlyUpdatedUsers: Usuaris actius fa poc
recentlyRegisteredUsers: Usuaris registrats fa poc
recentlyDiscoveredUsers: Nous suaris descoberts
administrator: Administrador
token: Token
registerSecurityKey: Registra una clau de seguretat
securityKeyName: Nom clau
lastUsed: Feta servir per última vegada
unregister: Anul·lar el registre
passwordLessLogin: Identificació sense contrasenya
share: Comparteix
notFound: No s'ha trobat
newPasswordIs: La nova contrasenya és "{password}"
notFoundDescription: No es pot trobar cap pàgina que correspongui a aquesta adreça
URL.
uploadFolder: Carpeta per defecte per pujar arxius
cacheClear: Netejar la memòria cau
markAsReadAllNotifications: Marca totes les notificacions com llegides
markAsReadAllUnreadNotes: Marca totes les publicacions com a llegides
markAsReadAllTalkMessages: Marca tots els missatges com llegits
help: Ajuda
inputMessageHere: Escriu aquí el missatge
close: Tancar
group: Grup
groups: Grups
createGroup: Crea un grup
ownedGroups: Grups que et pertanyen
joinedGroups: Grups als que t'has unit
groupName: Nom del grup
members: Membres
transfer: Transferir
messagingWithUser: Conversa privada
title: Títol
text: Text
enable: Activar
next: Següent
retype: Torna a entrar
noteOf: Publicat per {user}
inviteToGroup: Invitar a un grup
quoteAttached: Cita
quoteQuestion: Adjuntar com a cita?
noMessagesYet: Encara no hi han missatges
signinRequired: Si us plau registrat o inicia sessió per continuar
invitations: Invitacions
invitationCode: Codi d'invitació
checking: Comprovant...
usernameInvalidFormat: Pots fer servir lletres en majúscules o minúscules, nombres
i guions baixos.
tooShort: Massa curt
tooLong: Massa llarg
weakPassword: Contrasenya amb seguretat feble
strongPassword: Contrasenya amb seguretat forta
passwordMatched: Coincidències
signinWith: Inicieu sessió com {x}
signinFailed: No es pot iniciar sessió. El nom d'usuari o la contrasenya són incorrectes.
or: O
language: Idioma
uiLanguage: Idioma de la interfície d'usuari
groupInvited: T'han invitat a un grup
aboutX: Sobre {x}
youHaveNoGroups: No tens grups
disableDrawer: No facis servir els menús amb estil de calaix
noHistory: No ha historial disponible
signinHistory: Historial d'inicis de sessió
disableAnimatedMfm: Desactiva les animacions amb MFM
doing: Processant...
category: Categoría
existingAccount: El compte ja existeix
regenerate: Regenerar
docSource: Font d'aquest document
createAccount: Crear compte
fontSize: Mida del text
noFollowRequests: No tens cap sol·licitud de seguiment per aprovar
openImageInNewTab: Obre les imatges en una pestanya nova
dashboard: Panell
local: Local
remote: Remot
total: Total
weekOverWeekChanges: Canvis d'ençà la passada setmana
dayOverDayChanges: Canvis d'ençà ahir
appearance: Aparença
clientSettings: Configuració del client
accountSettings: Configuració del compte
promotion: Promogut
promote: Promoure
numberOfDays: Nombre de dies
objectStorageBaseUrl: Adreça URL base
hideThisNote: Amaga aquest article
showFeaturedNotesInTimeline: Mostra els articles destacats a la línea de temps
objectStorage: Emmagatzematge d'objectes
useObjectStorage: Fes servir l'emmagatzema d'objectes
expandTweet: Amplia el tuit
themeEditor: Editor de temes
description: Descripció
leaveConfirm: Hi han canvis que no s'han desat. Els vols descartar?
manage: Administració
plugins: Afegits
preferencesBackups: Preferències de còpies de seguretat
undeck: Treure el Deck
useBlurEffectForModal: Fes servir efectes de difuminació en les finestres modals
useFullReactionPicker: Fes servir el selector de reaccions a tamany complert
deck: Deck
width: Amplada
generateAccessToken: Genera un token d'accés
medium: Mitja
small: Petit
permission: Permisos
enableAll: Activa tots
tokenRequested: Garantir accés al compte
pluginTokenRequestedDescription: Aquest afegit podrà fer servir els permisos configurats
aquí.
emailServer: Servidor de correu electrònic
notificationType: Tipus de notificació
edit: Editar
emailAddress: Adreça de Correu electrònic
smtpConfig: Configuració del servidor SMTP
smtpHost: Host
enableEmail: Activa la distribució de correu electrònic
smtpPort: Port
emailConfigInfo: Fet servir per confirmar les adreçats de correu electrònic al registrar-se
o si s'oblida la contrasenya
email: Correu electrònic
smtpSecure: Fes servir SSL/TLS implícit per connectar-se per SMTP
emptyToDisableSmtpAuth: Deixa el nom d'usuari i la contrasenya sense emplenar per
desactivar la verificació SMTP
smtpSecureInfo: Desactiva això quant facis servir STARTTLS
testEmail: Envia un correu electrònic de verificació
wordMute: Silenciar paraules
regexpError: Error a la Expressió Regular
regexpErrorDescription: 'Hi ha un error a la expressió regular a la línea {line} de
la teva {tab} de paraules silenciades:'
userSaysSomething: '{name} va dir alguna cosa'
instanceMute: Silenciar instàncies
logs: Registres
copy: Copiar
delayed: Retardat
metrics: Mètriques
overview: Vista general
database: Base de dades
regenerateLoginToken: Regenera el token d'inici de sessió
reduceUiAnimation: Redueix les animacions de la UI
messagingWithGroup: Conversa en grup
invites: Invitacions
unavailable: No disponible
newMessageExists: Tens nous missatges
onlyOneFileCanBeAttached: Només pots adjuntar un fitxer per missatge
normalPassword: Contrasenya amb seguretat mitjana
passwordNotMatched: No hi han coincidències
useOsNativeEmojis: Fes servir els emojis per defecte del Sistema Operatiu
joinOrCreateGroup: Fes que et convidin a un grup o crea el teu propi.
objectStorageBaseUrlDesc: "Es l'adreça URL que serveix com a referència. Específica\
\ la adreça URL del CDN o Proxy si fas servir.\nPer fer servir S3 'https://<bucket>.s3.amazonaws.com'\
\ i per GCS o serveis semblants 'https://storage.googleapis.com/<bucket>', etc."
height: Alçada
large: Gran
notificationSetting: Preferències de notificacions
makeActive: Activar
notificationSettingDesc: Tria el tipus de notificació que es veure.
notifyAntenna: Notificar noves articles
withFileAntenna: Només articles amb fitxers
enableServiceworker: Activa les notificacions push per al teu navegador
antennaUsersDescription: Escriu un nom d'usuari per línea
antennaInstancesDescription: Escriu la adreça d'una instància per línea
tags: Etiquetes
antennaSource: Font de la antena
antennaKeywords: Paraules claus a escolta
antennaExcludeKeywords: Paraules clau a excluir
antennaKeywordsDescription: Separades amb espais per fer una condició AND i amb una
línea nova per fer una condició OR.
caseSensitive: Sensible a majúscules i minúscules
withReplies: Inclou respostes
connectedTo: Aquest(s) compte(s) estan connectats
silenceConfirm: Segur que vols posa en silenci aquest usuari?
unsilence: Desfés posar en silenci
unsilenceConfirm: Segur que vols treure el silenci a aquest usuari?
aboutMisskey: Sobre Calckey
twoStepAuthentication: Autentificació de dos factors
moderator: Moderador
moderation: Moderació
available: Disponible
tapSecurityKey: Escriu la teva clau de seguretat
nUsersMentioned: Esmentat per {n} usuari(s)
securityKey: Clau de seguretat
resetPassword: Restablir contrasenya
describeFile: Afegeix un subtítol
enterFileDescription: Entra un subtítol
author: Autor
disableAll: Desactiva tots
userSaysSomethingReason: '{name} va dir {reason}'
display: Visualització
channel: Canals
create: Crear
useGlobalSetting: Fes servir els ajustos globals
useGlobalSettingDesc: Si s'activa, es faran servir els ajustos de notificacions del
teu compte. Si es desactiva , es poden fer configuracions individuals.
other: Altres

View file

@ -1237,6 +1237,14 @@ _mfm:
sparkleDescription: "Gives content a sparkling particle effect."
rotate: "Rotate"
rotateDescription: "Turns content by a specified angle."
position: "Position"
positionDescription: "Move content by a specified amount."
scale: "Scale"
scaleDescription: "Scale content by a specified amount."
foreground: "Foreground color"
foregroundDescription: "Change the foreground color of text."
background: "Background color"
backgroundDescription: "Change the background color of text."
plain: "Plain"
plainDescription: "Deactivates the effects of all MFM contained within this MFM\
\ effect."

43
locales/fi.yml Normal file
View file

@ -0,0 +1,43 @@
username: Käyttäjänimi
fetchingAsApObject: Hae Fedeversestä
gotIt: Selvä!
cancel: Peruuta
enterUsername: Anna käyttäjänimi
renotedBy: Buustannut {käyttäjä}
noNotes: Ei lähetyksiä
noNotifications: Ei ilmoituksia
instance: Instanssi
settings: Asetukset
basicSettings: Perusasetukset
otherSettings: Muut asetukset
openInWindow: Avaa ikkunaan
profile: Profiili
timeline: Aikajana
noAccountDescription: Käyttäjä ei ole vielä kirjoittanut kuvaustaan vielä.
login: Kirjaudu sisään
loggingIn: Kirjautuu sisään
logout: Kirjaudu ulos
uploading: Tallentaa ylös...
save: Tallenna
favorites: Kirjanmerkit
unfavorite: Poista kirjanmerkeistä
favorited: Lisätty kirjanmerkkeihin.
alreadyFavorited: Lisätty jo kirjanmerkkeihin.
cantFavorite: Ei voitu lisätä kirjanmerkkeihin.
pin: Kiinnitä profiiliin
unpin: Irroita profiilista
delete: Poista
forgotPassword: Unohtunut salasana
search: Etsi
notifications: Ilmoitukset
password: Salasana
ok: OK
noThankYou: Ei kiitos
signup: Rekisteröidy
users: Käyttäjät
addUser: Lisää käyttäjä
addInstance: Lisää instanssi
favorite: Lisää kirjanmerkkeihin
copyContent: Kopioi sisältö
deleteAndEdit: Poista ja muokkaa
copyLink: Kopioi linkki

View file

@ -986,7 +986,7 @@ _registry:
createKey: "Новый ключ"
_aboutMisskey:
about: "Calckey это форк Misskey, сделанный ThatOneCalculator, разработка которого\
\ начал с 2022."
\ началась с 2022."
contributors: "Основные соавторы"
allContributors: "Все соавторы"
source: "Исходный код"

View file

@ -64,7 +64,7 @@ import: "匯入"
export: "匯出"
files: "檔案"
download: "下載"
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。\n"
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。"
unfollowConfirm: "確定要取消追隨{name}嗎?"
exportRequested: "已請求匯出。這可能會花一點時間。結束後檔案將會被放到雲端裡。"
importRequested: "已請求匯入。這可能會花一點時間"
@ -291,7 +291,7 @@ emptyDrive: "雲端硬碟為空"
emptyFolder: "資料夾為空"
unableToDelete: "無法刪除"
inputNewFileName: "輸入檔案名稱"
inputNewDescription: "請輸入新標題 "
inputNewDescription: "請輸入新標題"
inputNewFolderName: "輸入新資料夾的名稱"
circularReferenceFolder: "目標文件夾是您要移動的文件夾的子文件夾。"
hasChildFilesOrFolders: "此文件夾不是空的,無法刪除。"
@ -324,7 +324,7 @@ yearX: "{year}年"
pages: "頁面"
integration: "整合"
connectService: "己連結"
disconnectService: "己斷開 "
disconnectService: "己斷開"
enableLocalTimeline: "開啟本地時間軸"
enableGlobalTimeline: "啟用公開時間軸"
disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和協調人仍可以繼續使用,以方便您。"
@ -336,7 +336,7 @@ driveCapacityPerRemoteAccount: "每個非本地用戶的雲端容量"
inMb: "以Mbps為單位"
iconUrl: "圖像URL"
bannerUrl: "橫幅圖像URL"
backgroundImageUrl: "背景圖片的來源網址 "
backgroundImageUrl: "背景圖片的來源網址"
basicInfo: "基本資訊"
pinnedUsers: "置頂用戶"
pinnedUsersDescription: "在「發現」頁面中使用換行標記想要置頂的使用者。"
@ -490,7 +490,7 @@ useObjectStorage: "使用Object Storage"
objectStorageBaseUrl: "Base URL"
objectStorageBaseUrlDesc: "引用時的URL。如果您使用的是CDN或反向代理请指定其URL例如S3“https://<bucket>.s3.amazonaws.com”GCS“https://storage.googleapis.com/<bucket>”"
objectStorageBucket: "儲存空間Bucket"
objectStorageBucketDesc: "請指定您正在使用的服務的存儲桶名稱。 "
objectStorageBucketDesc: "請指定您正在使用的服務的存儲桶名稱。"
objectStoragePrefix: "前綴"
objectStoragePrefixDesc: "它存儲在此前綴目錄下。"
objectStorageEndpoint: "端點Endpoint"
@ -560,8 +560,8 @@ disablePlayer: "關閉播放器"
expandTweet: "展開推文"
themeEditor: "主題編輯器"
description: "描述"
describeFile: "添加標題 "
enterFileDescription: "輸入標題 "
describeFile: "添加標題"
enterFileDescription: "輸入標題"
author: "作者"
leaveConfirm: "有未保存的更改。要放棄嗎?"
manage: "管理"
@ -865,7 +865,7 @@ driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限"
driveCapOverrideCaption: "如果指定0以下的值就會被取消。"
requireAdminForView: "必須以管理者帳號登入才可以檢視。"
isSystemAccount: "由系統自動建立與管理的帳號。"
typeToConfirm: "要執行這項操作,請輸入 {x} "
typeToConfirm: "要執行這項操作,請輸入 {x}"
deleteAccount: "刪除帳號"
document: "文件"
numberOfPageCache: "快取頁面數"
@ -876,7 +876,7 @@ statusbar: "狀態列"
pleaseSelect: "請選擇"
reverse: "翻轉"
colored: "彩色"
refreshInterval: "更新間隔"
refreshInterval: "更新間隔 "
label: "標籤"
type: "類型"
speed: "速度"
@ -895,7 +895,7 @@ activeEmailValidationDescription: "積極地驗證用戶的電子郵件地址,
navbar: "導覽列"
shuffle: "隨機"
account: "帳戶"
move: "移動 "
move: "移動"
customKaTeXMacro: "自定義 KaTeX 宏"
customKaTeXMacroDescription: "使用宏來輕鬆的輸入數學表達式吧!宏的用法與 LaTeX 中的命令定義相同。你可以使用 \\newcommand{\\\
name}{content} 或 \\newcommand{\\name}[number of arguments]{content} 來輸入數學表達式。舉個例子,\\\
@ -933,11 +933,11 @@ _accountDelete:
inProgress: "正在刪除"
_ad:
back: "返回"
reduceFrequencyOfThisAd: "降低此廣告的頻率 "
reduceFrequencyOfThisAd: "降低此廣告的頻率"
_forgotPassword:
enterEmail: "請輸入您的帳戶註冊的電子郵件地址。 密碼重置連結將被發送到該電子郵件地址。"
ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。 "
contactAdmin: "此實例不支持電子郵件,請聯繫您的管理員重置您的密碼。 "
ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。"
contactAdmin: "此實例不支持電子郵件,請聯繫您的管理員重置您的密碼。"
_gallery:
my: "我的貼文"
liked: "喜歡的貼文"
@ -1000,7 +1000,7 @@ _mfm:
url: "URL"
urlDescription: "可以展示URL位址。"
link: "鏈接"
linkDescription: "您可以將特定範圍的文章與 URL 相關聯。 "
linkDescription: "您可以將特定範圍的文章與 URL 相關聯。"
bold: "粗體"
boldDescription: "可以將文字顯示为粗體来強調。"
small: "縮小"
@ -1805,3 +1805,6 @@ migration: 遷移
homeTimeline: 主頁時間軸
swipeOnDesktop: 允許在桌面上進行手機式滑動
logoImageUrl: 圖標網址
addInstance: 增加一個實例
noInstances: 沒有實例
flagSpeakAsCat: 像貓一樣地說話

View file

@ -1,6 +1,6 @@
{
"name": "calckey",
"version": "13.2.0-beta8",
"version": "13.2.0-rc",
"codename": "aqua",
"repository": {
"type": "git",
@ -40,6 +40,8 @@
"@bull-board/ui": "^4.10.2",
"@napi-rs/cli": "^2.15.0",
"@tensorflow/tfjs": "^3.21.0",
"focus-trap": "^7.2.0",
"focus-trap-vue": "^4.0.1",
"js-yaml": "4.1.0",
"seedrandom": "^3.0.5"
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -26,7 +26,7 @@
"@bull-board/api": "^4.6.4",
"@bull-board/koa": "^4.6.4",
"@bull-board/ui": "^4.6.4",
"@calckey/megalodon": "5.1.24",
"@calckey/megalodon": "5.2.0",
"@discordapp/twemoji": "14.0.2",
"@elastic/elasticsearch": "7.17.0",
"@koa/cors": "3.4.3",
@ -75,9 +75,11 @@
"koa": "2.13.4",
"koa-body": "^6.0.1",
"koa-bodyparser": "4.3.0",
"koa-favicon": "2.1.0",
"koa-json-body": "5.3.0",
"koa-logger": "3.2.1",
"koa-mount": "4.0.0",
"koa-remove-trailing-slashes": "2.0.3",
"koa-send": "5.0.1",
"koa-slow": "2.1.0",
"koa-views": "7.0.2",

View file

@ -20,6 +20,7 @@ import handler from "./api-handler.js";
import signup from "./private/signup.js";
import signin from "./private/signin.js";
import signupPending from "./private/signup-pending.js";
import verifyEmail from "./private/verify-email.js";
import discord from "./service/discord.js";
import github from "./service/github.js";
import twitter from "./service/twitter.js";
@ -177,6 +178,7 @@ for (const endpoint of [...endpoints, ...compatibility]) {
router.post("/signup", signup);
router.post("/signin", signin);
router.post("/signup-pending", signupPending);
router.post("/verify-email", verifyEmail);
router.use(discord.routes());
router.use(github.routes());

View file

@ -91,6 +91,44 @@ export function apiAccountMastodon(router: Router): void {
ctx.body = e.response.data;
}
});
router.get("/v1/accounts/relationships", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
let users;
try {
// TODO: this should be body
let ids = ctx.request.query ? ctx.request.query["id[]"] : null;
if (typeof ids === "string") {
ids = [ids];
}
users = ids;
relationshipModel.id = ids?.toString() || "1";
if (!ids) {
ctx.body = [relationshipModel];
return;
}
let reqIds = [];
for (let i = 0; i < ids.length; i++) {
reqIds.push(convertId(ids[i], IdType.CalckeyId));
}
const data = await client.getRelationships(reqIds);
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} catch (e: any) {
console.error(e);
let data = e.response.data;
data.users = users;
console.error(data);
ctx.status = 401;
ctx.body = data;
}
});
router.get<{ Params: { id: string } }>("/v1/accounts/:id", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization;
@ -340,44 +378,6 @@ export function apiAccountMastodon(router: Router): void {
}
},
);
router.get("/v1/accounts/relationships", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
let users;
try {
// TODO: this should be body
let ids = ctx.request.query ? ctx.request.query["id[]"] : null;
if (typeof ids === "string") {
ids = [ids];
}
users = ids;
relationshipModel.id = ids?.toString() || "1";
if (!ids) {
ctx.body = [relationshipModel];
return;
}
let reqIds = [];
for (let i = 0; i < ids.length; i++) {
reqIds.push(convertId(ids[i], IdType.CalckeyId));
}
const data = await client.getRelationships(reqIds);
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} catch (e: any) {
console.error(e);
let data = e.response.data;
data.users = users;
console.error(data);
ctx.status = 401;
ctx.body = data;
}
});
router.get("/v1/bookmarks", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization;

View file

@ -388,7 +388,7 @@ export function statusModel(
emojis: MastodonEntity.Emoji[],
content: string,
) {
const now = Math.floor(new Date().getTime() / 1000);
const now = new Date().toISOString();
return {
id: "9atm5frjhb",
uri: "https://http.cat/404", // ""

View file

@ -211,7 +211,7 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.createList((ctx.query as any).title);
const data = await client.createList((ctx.request.body as any).title);
ctx.body = data.data;
} catch (e: any) {
console.error(e);
@ -227,7 +227,7 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.updateList(ctx.params.id, ctx.query as any);
const data = await client.updateList(ctx.params.id, (ctx.request.body as any).title);
ctx.body = data.data;
} catch (e: any) {
console.error(e);

View file

@ -0,0 +1,38 @@
import type Koa from "koa";
import { Users, UserProfiles } from "@/models/index.js";
import { publishMainStream } from "@/services/stream.js";
export default async (ctx: Koa.Context) => {
const body = ctx.request.body;
const code = body["code"];
const profile = await UserProfiles.findOneByOrFail({ emailVerifyCode: code });
if (profile != null) {
ctx.body = "Verify succeeded!";
await UserProfiles.update(
{ userId: profile.userId },
{
emailVerified: true,
emailVerifyCode: null,
},
);
publishMainStream(
profile.userId,
"meUpdated",
await Users.pack(
profile.userId,
{ id: profile.userId },
{
detail: true,
includeSecrets: true,
},
),
);
} else {
ctx.throw(404);
}
};

View file

@ -10,6 +10,7 @@ import Router from "@koa/router";
import mount from "koa-mount";
import koaLogger from "koa-logger";
import * as slow from "koa-slow";
import { IsNull } from "typeorm";
import config from "@/config/index.js";
import Logger from "@/services/logger.js";
@ -29,6 +30,7 @@ import proxyServer from "./proxy/index.js";
import webServer from "./web/index.js";
import { initializeStreamingServer } from "./api/streaming.js";
import { koaBody } from "koa-body";
import removeTrailingSlash from "koa-remove-trailing-slashes";
import { v4 as uuid } from "uuid";
export const serverLogger = new Logger("server", "gray", false);
@ -37,12 +39,7 @@ export const serverLogger = new Logger("server", "gray", false);
const app = new Koa();
app.proxy = true;
// Replace trailing slashes
app.use(async (ctx, next) => {
if (ctx.request.path !== "/" && ctx.request.path.endsWith("/"))
return ctx.redirect(ctx.request.path.replace(/\/$/, ""));
else await next();
});
app.use(removeTrailingSlash());
if (!["production", "test"].includes(process.env.NODE_ENV || "")) {
// Logger
@ -127,40 +124,6 @@ router.get("/identicon/:x", async (ctx) => {
ctx.body = fs.createReadStream(temp).on("close", () => cleanup());
});
router.get("/verify-email/:code", async (ctx) => {
const profile = await UserProfiles.findOneBy({
emailVerifyCode: ctx.params.code,
});
if (profile != null) {
ctx.body = "Verify succeeded!";
ctx.status = 200;
await UserProfiles.update(
{ userId: profile.userId },
{
emailVerified: true,
emailVerifyCode: null,
},
);
publishMainStream(
profile.userId,
"meUpdated",
await Users.pack(
profile.userId,
{ id: profile.userId },
{
detail: true,
includeSecrets: true,
},
),
);
} else {
ctx.status = 404;
}
});
mastoRouter.get("/oauth/authorize", async (ctx) => {
const { client_id, state, redirect_uri } = ctx.request.query;
console.log(ctx.request.req);

View file

@ -8,11 +8,13 @@ import { readFileSync } from "node:fs";
import Koa from "koa";
import Router from "@koa/router";
import send from "koa-send";
import favicon from "koa-favicon";
import views from "koa-views";
import sharp from "sharp";
import { createBullBoard } from "@bull-board/api";
import { BullAdapter } from "@bull-board/api/bullAdapter.js";
import { KoaAdapter } from "@bull-board/koa";
import { In, IsNull } from "typeorm";
import { fetchMeta } from "@/misc/fetch-meta.js";
import config from "@/config/index.js";
@ -96,14 +98,8 @@ app.use(
}),
);
// Favicon Router
app.use(async (ctx, next) => {
if (ctx.path != "/favicon.ico") return next();
const meta = await fetchMeta();
if (meta.iconUrl === "")
ctx.body = readFileSync(`${_dirname}/../../../assets/favicon.ico`);
else ctx.redirect(meta.iconUrl);
});
// Serve favicon
app.use(favicon(`${_dirname}/../../../assets/favicon.ico`));
// Common request handler
app.use(async (ctx, next) => {

View file

@ -33,7 +33,6 @@ export async function sendEmail(
} as any);
try {
// TODO: htmlサニタイズ
const info = await transporter.sendMail({
from: meta.email!,
to: to,
@ -44,81 +43,23 @@ export async function sendEmail(
<head>
<meta charset="utf-8">
<title>${subject}</title>
<style>
html {
background: #191724;
}
body {
padding: 16px;
margin: 0;
font-family: sans-serif;
font-size: 14px;
}
a {
text-decoration: none;
color: #31748f;
}
a:hover {
text-decoration: underline;
}
main {
max-width: 500px;
margin: 0 auto;
background: #1f1d2e;
color: #e0def4;
}
main > header {
padding: 32px;
background: #31748f;
display: flex;
}
main > header > img {
max-width: 128px;
max-height: 72px;
vertical-align: bottom;
margin-right: 16px;
}
main > article {
padding: 32px;
}
main > article > h1 {
margin: 0 0 1em 0;
}
main > footer {
padding: 32px;
border-top: solid 1px #26233a;
}
nav {
box-sizing: border-box;
max-width: 500px;
margin: 16px auto 0 auto;
padding: 0 32px;
}
nav > a {
color: #6e6a86;
}
</style>
</head>
<body>
<main>
<header>
<img src="${meta.logoImageUrl || meta.iconUrl || iconUrl}" height="100"/>
<h1>${meta.name}</h1>
<body style="background: #191724; padding: 16px; margin: 0; font-family: sans-serif; font-size: 14px;">
<main style="max-width: 500px; margin: 0 auto; background: #1f1d2e; color: #e0def4;">
<header style="padding: 32px; background: #31748f; display: flex;">
<img src="${meta.logoImageUrl || meta.iconUrl || iconUrl}" style="max-width: 128px; max-height: 72px; vertical-align: bottom; margin-right: 16px;"/>
<h1 style="margin: 0 0 1em 0;">${meta.name}</h1>
</header>
<article>
<article style="padding: 32px;">
<h1>${subject}</h1>
<div>${html}</div>
</article>
<footer>
<a href="${emailSettingUrl}">${"Email setting"}</a>
<footer style="padding: 32px; border-top: solid 1px #26233a;">
<a href="${emailSettingUrl}" style="color: #31748f !important;">${"Email setting"}</a>
</footer>
</main>
<nav>
<a href="${config.url}">${config.host}</a>
<nav style="box-sizing: border-box; max-width: 500px; margin: 16px auto 0 auto; padding: 0 32px;">
<a href="${config.url}" style="color: #6e6a86 !important;">${config.host}</a>
</nav>
</body>
</html>`,

View file

@ -195,8 +195,7 @@ function onMousedown(evt: MouseEvent): void {
}
&:focus-visible {
outline: solid 2px var(--focus);
outline-offset: 2px;
outline: auto;
}
&.inline {

View file

@ -1,5 +1,6 @@
<template>
<button
ref="el"
class="_button"
:class="{ showLess: modelValue, fade: !modelValue }"
@click.stop="toggle"
@ -12,7 +13,7 @@
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { computed, ref } from "vue";
import { length } from "stringz";
import * as misskey from "calckey-js";
import { concat } from "@/scripts/array";
@ -27,6 +28,8 @@ const emit = defineEmits<{
(ev: "update:modelValue", v: boolean): void;
}>();
const el = ref<HTMLElement>();
const label = computed(() => {
return concat([
props.note.text
@ -43,6 +46,14 @@ const label = computed(() => {
const toggle = () => {
emit("update:modelValue", !props.modelValue);
};
function focus() {
el.value.focus();
}
defineExpose({
focus
});
</script>
<style lang="scss" scoped>
@ -62,9 +73,46 @@ const toggle = () => {
}
}
}
&:hover > span {
&:hover > span, &:focus > span {
background: var(--cwFg) !important;
color: var(--cwBg) !important;
}
&.fade {
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
z-index: 2;
> span {
display: inline-block;
background: var(--panel);
padding: 0.4em 1em;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
}
&:hover, &:focus {
> span {
background: var(--panelHighlight);
}
}
}
&.showLess {
width: 100%;
margin-top: 1em;
position: sticky;
bottom: var(--stickyBottom);
> span {
display: inline-block;
background: var(--panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 0 7px 7px var(--bg);
}
}
}
</style>

View file

@ -95,7 +95,7 @@ export default defineComponent({
h(MkAd, {
class: "a", // advertise()
key: item.id + ":ad",
prefer: ["horizontal", "horizontal-big"],
prefer: ["inline", "inline-big"],
}),
el,
];

View file

@ -1,5 +1,5 @@
<template>
<div ref="thumbnail" class="zdjebgpv">
<button ref="thumbnail" class="zdjebgpv">
<ImgWithBlurhash
v-if="isThumbnailAvailable"
:hash="file.blurhash"
@ -36,7 +36,7 @@
v-if="isThumbnailAvailable && is === 'video'"
class="ph-file-video ph-bold ph-lg icon-sub"
></i>
</div>
</button>
</template>
<script lang="ts" setup>
@ -88,6 +88,9 @@ const isThumbnailAvailable = computed(() => {
background: var(--panel);
border-radius: 8px;
overflow: clip;
border: 0;
padding: 0;
cursor: pointer;
> .icon-sub {
position: absolute;

View file

@ -1,157 +1,160 @@
<template>
<div
class="omfetrab"
:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
>
<input
ref="search"
v-model.trim="q"
class="search"
data-prevent-emoji-insert
:class="{ filled: q != null && q != '' }"
:placeholder="i18n.ts.search"
type="search"
@paste.stop="paste"
@keyup.enter="done()"
/>
<div ref="emojis" class="emojis">
<section class="result">
<div v-if="searchResultCustom.length > 0" class="body">
<button
v-for="emoji in searchResultCustom"
:key="emoji.id"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
<img
class="emoji"
:src="
disableShowingAnimatedImages
? getStaticImageUrl(emoji.url)
: emoji.url
"
/>
</button>
</div>
<div v-if="searchResultUnicode.length > 0" class="body">
<button
v-for="emoji in searchResultUnicode"
:key="emoji.name"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkEmoji class="emoji" :emoji="emoji.char" />
</button>
</div>
</section>
<div v-if="tab === 'index'" class="group index">
<section v-if="showPinned">
<div class="body">
<FocusTrap v-bind:active="isActive">
<div
class="omfetrab"
:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
tabindex="-1"
>
<input
ref="search"
v-model.trim="q"
class="search"
data-prevent-emoji-insert
:class="{ filled: q != null && q != '' }"
:placeholder="i18n.ts.search"
type="search"
@paste.stop="paste"
@keyup.enter="done()"
/>
<div ref="emojis" class="emojis">
<section class="result">
<div v-if="searchResultCustom.length > 0" class="body">
<button
v-for="emoji in pinned"
:key="emoji"
v-for="emoji in searchResultCustom"
:key="emoji.id"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkEmoji
<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
<img
class="emoji"
:emoji="emoji"
:normal="true"
:src="
disableShowingAnimatedImages
? getStaticImageUrl(emoji.url)
: emoji.url
"
/>
</button>
</div>
<div v-if="searchResultUnicode.length > 0" class="body">
<button
v-for="emoji in searchResultUnicode"
:key="emoji.name"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkEmoji class="emoji" :emoji="emoji.char" />
</button>
</div>
</section>
<section>
<header class="_acrylic">
<i class="ph-alarm ph-bold ph-fw ph-lg"></i>
{{ i18n.ts.recentUsed }}
</header>
<div class="body">
<button
v-for="emoji in recentlyUsedEmojis"
:key="emoji"
class="_button item"
@click="chosen(emoji, $event)"
>
<MkEmoji
class="emoji"
:emoji="emoji"
:normal="true"
/>
</button>
</div>
</section>
<div v-if="tab === 'index'" class="group index">
<section v-if="showPinned">
<div class="body">
<button
v-for="emoji in pinned"
:key="emoji"
class="_button item"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkEmoji
class="emoji"
:emoji="emoji"
:normal="true"
/>
</button>
</div>
</section>
<section>
<header class="_acrylic">
<i class="ph-alarm ph-bold ph-fw ph-lg"></i>
{{ i18n.ts.recentUsed }}
</header>
<div class="body">
<button
v-for="emoji in recentlyUsedEmojis"
:key="emoji"
class="_button item"
@click="chosen(emoji, $event)"
>
<MkEmoji
class="emoji"
:emoji="emoji"
:normal="true"
/>
</button>
</div>
</section>
</div>
<div v-once class="group">
<header>{{ i18n.ts.customEmojis }}</header>
<XSection
v-for="category in customEmojiCategories"
:key="'custom:' + category"
:initial-shown="false"
:emojis="
customEmojis
.filter((e) => e.category === category)
.map((e) => ':' + e.name + ':')
"
@chosen="chosen"
>{{ category || i18n.ts.other }}</XSection
>
</div>
<div v-once class="group">
<header>{{ i18n.ts.emoji }}</header>
<XSection
v-for="category in categories"
:key="category"
:emojis="
emojilist
.filter((e) => e.category === category)
.map((e) => e.char)
"
@chosen="chosen"
>{{ category }}</XSection
>
</div>
</div>
<div v-once class="group">
<header>{{ i18n.ts.customEmojis }}</header>
<XSection
v-for="category in customEmojiCategories"
:key="'custom:' + category"
:initial-shown="false"
:emojis="
customEmojis
.filter((e) => e.category === category)
.map((e) => ':' + e.name + ':')
"
@chosen="chosen"
>{{ category || i18n.ts.other }}</XSection
<div class="tabs">
<button
class="_button tab"
:class="{ active: tab === 'index' }"
@click="tab = 'index'"
>
</div>
<div v-once class="group">
<header>{{ i18n.ts.emoji }}</header>
<XSection
v-for="category in categories"
:key="category"
:emojis="
emojilist
.filter((e) => e.category === category)
.map((e) => e.char)
"
@chosen="chosen"
>{{ category }}</XSection
<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
</button>
<button
class="_button tab"
:class="{ active: tab === 'custom' }"
@click="tab = 'custom'"
>
<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
</button>
<button
class="_button tab"
:class="{ active: tab === 'unicode' }"
@click="tab = 'unicode'"
>
<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
</button>
<button
class="_button tab"
:class="{ active: tab === 'tags' }"
@click="tab = 'tags'"
>
<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
</button>
</div>
</div>
<div class="tabs">
<button
class="_button tab"
:class="{ active: tab === 'index' }"
@click="tab = 'index'"
>
<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
</button>
<button
class="_button tab"
:class="{ active: tab === 'custom' }"
@click="tab = 'custom'"
>
<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
</button>
<button
class="_button tab"
:class="{ active: tab === 'unicode' }"
@click="tab = 'unicode'"
>
<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
</button>
<button
class="_button tab"
:class="{ active: tab === 'tags' }"
@click="tab = 'tags'"
>
<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
</button>
</div>
</div>
</FocusTrap>
</template>
<script lang="ts" setup>
@ -171,6 +174,7 @@ import { deviceKind } from "@/scripts/device-kind";
import { emojiCategories, instance } from "@/instance";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
import { FocusTrap } from 'focus-trap-vue';
const props = withDefaults(
defineProps<{

View file

@ -20,9 +20,12 @@ export default defineComponent({
},
computed: {
compiledFormula(): any {
return katex.renderToString(this.formula, {
const katexString = katex.renderToString(this.formula, {
throwOnError: false,
} as any);
return this.block
? `<div style="text-align:center">${katexString}</div>`
: katexString;
},
},
});

View file

@ -139,7 +139,7 @@ function close() {
height: 100px;
border-radius: 10px;
&:hover {
&:hover, &:focus-visible {
color: var(--accent);
background: var(--accentedBg);
text-decoration: none;

View file

@ -138,6 +138,10 @@ watch(
background-position: center;
background-size: contain;
background-repeat: no-repeat;
box-sizing: border-box;
&:focus-visible {
border: 2px solid var(--accent);
}
> .gif {
background-color: var(--fg);

View file

@ -1,14 +1,14 @@
<template>
<div ref="el" class="sfhdhdhr">
<MkMenu
ref="menu"
:items="items"
:align="align"
:width="width"
:as-drawer="false"
@close="onChildClosed"
/>
</div>
<div ref="el" class="sfhdhdhr" tabindex="-1">
<MkMenu
ref="menu"
:items="items"
:align="align"
:width="width"
:as-drawer="false"
@close="onChildClosed"
/>
</div>
</template>
<script lang="ts" setup>
@ -23,7 +23,6 @@ import {
} from "vue";
import MkMenu from "./MkMenu.vue";
import { MenuItem } from "@/types/menu";
import * as os from "@/os";
const props = defineProps<{
items: MenuItem[];

View file

@ -1,191 +1,188 @@
<template>
<div>
<div
ref="itemsEl"
v-hotkey="keymap"
class="rrevdjwt _popup _shadow"
:class="{ center: align === 'center', asDrawer }"
:style="{
width: width && !asDrawer ? width + 'px' : '',
maxHeight: maxHeight ? maxHeight + 'px' : '',
}"
@contextmenu.self="(e) => e.preventDefault()"
>
<template v-for="(item, i) in items2">
<div v-if="item === null" class="divider"></div>
<span v-else-if="item.type === 'label'" class="label item">
<span :style="item.textStyle || ''">{{ item.text }}</span>
</span>
<span
v-else-if="item.type === 'pending'"
:tabindex="i"
class="pending item"
>
<span><MkEllipsis /></span>
</span>
<MkA
v-else-if="item.type === 'link'"
:to="item.to"
:tabindex="i"
class="_button item"
@click.passive="close(true)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
<FocusTrap v-bind:active="isActive">
<div tabindex="-1" v-focus>
<div
ref="itemsEl"
class="rrevdjwt _popup _shadow"
:class="{ center: align === 'center', asDrawer }"
:style="{
width: width && !asDrawer ? width + 'px' : '',
maxHeight: maxHeight ? maxHeight + 'px' : '',
}"
@contextmenu.self="(e) => e.preventDefault()"
>
<template v-for="(item, i) in items2">
<div v-if="item === null" class="divider"></div>
<span v-else-if="item.type === 'label'" class="label item">
<span :style="item.textStyle || ''">{{ item.text }}</span>
</span>
<MkAvatar
v-if="item.avatar"
:user="item.avatar"
class="avatar"
/>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</MkA>
<a
v-else-if="item.type === 'a'"
:href="item.href"
:target="item.target"
:download="item.download"
:tabindex="i"
class="_button item"
@click="close(true)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</a>
<button
v-else-if="item.type === 'user' && !items.hidden"
:tabindex="i"
class="_button item"
:class="{ active: item.active }"
:disabled="item.active"
@click="clicked(item.action, $event)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<MkAvatar :user="item.user" class="avatar" /><MkUserName
:user="item.user"
/>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</button>
<span
v-else-if="item.type === 'switch'"
:tabindex="i"
class="item"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<FormSwitch
v-model="item.ref"
:disabled="item.disabled"
class="form-switch"
:style="item.textStyle || ''"
>{{ item.text }}</FormSwitch
<span
v-else-if="item.type === 'pending'"
class="pending item"
>
<span><MkEllipsis /></span>
</span>
<MkA
v-else-if="item.type === 'link'"
:to="item.to"
class="_button item"
@click.passive="close(true)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<MkAvatar
v-if="item.avatar"
:user="item.avatar"
class="avatar"
disableLink
/>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</MkA>
<a
v-else-if="item.type === 'a'"
:href="item.href"
:target="item.target"
:download="item.download"
class="_button item"
@click="close(true)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</a>
<button
v-else-if="item.type === 'user' && !items.hidden"
class="_button item"
:class="{ active: item.active }"
:disabled="item.active"
@click="clicked(item.action, $event)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<MkAvatar :user="item.user" class="avatar" disableLink /><MkUserName
:user="item.user"
/>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</button>
<span
v-else-if="item.type === 'switch'"
class="item"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<FormSwitch
v-model="item.ref"
:disabled="item.disabled"
class="form-switch"
:style="item.textStyle || ''"
>{{ item.text }}</FormSwitch
>
</span>
<button
v-else-if="item.type === 'parent'"
class="_button item parent"
:class="{ childShowing: childShowingItem === item }"
@mouseenter="showChildren(item, $event)"
@click="showChildren(item, $event)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span class="caret"
><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
></span>
</button>
<button
v-else-if="!item.hidden"
class="_button item"
:class="{ danger: item.danger, active: item.active }"
:disabled="item.active"
@click="clicked(item.action, $event)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<MkAvatar
v-if="item.avatar"
:user="item.avatar"
class="avatar"
disableLink
/>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</button>
</template>
<span v-if="items2.length === 0" class="none item">
<span>{{ i18n.ts.none }}</span>
</span>
<button
v-else-if="item.type === 'parent'"
:tabindex="i"
class="_button item parent"
:class="{ childShowing: childShowingItem === item }"
@mouseenter="showChildren(item, $event)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span class="caret"
><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
></span>
</button>
<button
v-else-if="!item.hidden"
:tabindex="i"
class="_button item"
:class="{ danger: item.danger, active: item.active }"
:disabled="item.active"
@click="clicked(item.action, $event)"
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<i
v-if="item.icon"
class="ph-fw ph-lg"
:class="item.icon"
></i>
<span v-else-if="item.icons">
<i
v-for="icon in item.icons"
class="ph-fw ph-lg"
:class="icon"
></i>
</span>
<MkAvatar
v-if="item.avatar"
:user="item.avatar"
class="avatar"
/>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</button>
</template>
<span v-if="items2.length === 0" class="none item">
<span>{{ i18n.ts.none }}</span>
</span>
</div>
<div v-if="childMenu" class="child">
<XChild
ref="child"
:items="childMenu"
:target-element="childTarget"
:root-element="itemsEl"
showing
@actioned="childActioned"
/>
</div>
</div>
<div v-if="childMenu" class="child">
<XChild
ref="child"
:items="childMenu"
:target-element="childTarget"
:root-element="itemsEl"
showing
@actioned="childActioned"
/>
</div>
</div>
</FocusTrap>
</template>
<script lang="ts" setup>
@ -206,6 +203,7 @@ import FormSwitch from "@/components/form/switch.vue";
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu";
import * as os from "@/os";
import { i18n } from "@/i18n";
import { FocusTrap } from 'focus-trap-vue';
const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
@ -228,12 +226,6 @@ let items2: InnerMenuItem[] = $ref([]);
let child = $ref<InstanceType<typeof XChild>>();
let keymap = computed(() => ({
"up|k|shift+tab": focusUp,
"down|j|tab": focusDown,
esc: close,
}));
let childShowingItem = $ref<MenuItem | null>();
watch(
@ -364,8 +356,7 @@ onBeforeUnmount(() => {
font-size: 0.9em;
line-height: 20px;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
outline: none;
&:before {
content: "";
@ -389,7 +380,7 @@ onBeforeUnmount(() => {
transform: translateY(0em);
}
&:not(:disabled):hover {
&:not(:disabled):hover, &:focus-visible {
color: var(--accent);
text-decoration: none;
@ -397,6 +388,9 @@ onBeforeUnmount(() => {
background: var(--accentedBg);
}
}
&:focus-visible:before {
outline: auto;
}
&.danger {
color: #eb6f92;

View file

@ -14,54 +14,59 @@
:duration="transitionDuration"
appear
@after-leave="emit('closed')"
@keyup.esc="emit('click')"
@enter="emit('opening')"
@after-enter="onOpened"
>
<div
v-show="manualShowing != null ? manualShowing : showing"
v-hotkey.global="keymap"
:class="[
$style.root,
{
[$style.drawer]: type === 'drawer',
[$style.dialog]: type === 'dialog' || type === 'dialog:top',
[$style.popup]: type === 'popup',
},
]"
:style="{
zIndex,
pointerEvents: (manualShowing != null ? manualShowing : showing)
? 'auto'
: 'none',
'--transformOrigin': transformOrigin,
}"
>
<FocusTrap v-model:active="isActive">
<div
class="_modalBg data-cy-bg"
v-show="manualShowing != null ? manualShowing : showing"
v-hotkey.global="keymap"
:class="[
$style.bg,
$style.root,
{
[$style.bgTransparent]: isEnableBgTransparent,
'data-cy-transparent': isEnableBgTransparent,
[$style.drawer]: type === 'drawer',
[$style.dialog]: type === 'dialog' || type === 'dialog:top',
[$style.popup]: type === 'popup',
},
]"
:style="{ zIndex }"
@click="onBgClick"
@mousedown="onBgClick"
@contextmenu.prevent.stop="() => {}"
></div>
<div
ref="content"
:class="[
$style.content,
{ [$style.fixed]: fixed, top: type === 'dialog:top' },
]"
:style="{ zIndex }"
@click.self="onBgClick"
:style="{
zIndex,
pointerEvents: (manualShowing != null ? manualShowing : showing)
? 'auto'
: 'none',
'--transformOrigin': transformOrigin,
}"
tabindex="-1"
v-focus
>
<slot :max-height="maxHeight" :type="type"></slot>
<div
class="_modalBg data-cy-bg"
:class="[
$style.bg,
{
[$style.bgTransparent]: isEnableBgTransparent,
'data-cy-transparent': isEnableBgTransparent,
},
]"
:style="{ zIndex }"
@click="onBgClick"
@mousedown="onBgClick"
@contextmenu.prevent.stop="() => {}"
></div>
<div
ref="content"
:class="[
$style.content,
{ [$style.fixed]: fixed, top: type === 'dialog:top' },
]"
:style="{ zIndex }"
@click.self="onBgClick"
>
<slot :max-height="maxHeight" :type="type"></slot>
</div>
</div>
</div>
</FocusTrap>
</Transition>
</template>
@ -71,6 +76,7 @@ import * as os from "@/os";
import { isTouchUsing } from "@/scripts/touch";
import { defaultStore } from "@/store";
import { deviceKind } from "@/scripts/device-kind";
import { FocusTrap } from 'focus-trap-vue';
function getFixedContainer(el: Element | null): Element | null {
if (el == null || el.tagName === "BODY") return null;
@ -166,6 +172,7 @@ let transitionDuration = $computed(() =>
let contentClicking = false;
const focusedElement = document.activeElement;
function close(opts: { useSendAnimation?: boolean } = {}) {
if (opts.useSendAnimation) {
useSendAnime = true;
@ -175,10 +182,12 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
if (props.src) props.src.style.pointerEvents = "auto";
showing = false;
emit("close");
focusedElement.focus();
}
function onBgClick() {
if (contentClicking) return;
focusedElement.focus();
emit("click");
}
@ -481,6 +490,7 @@ defineExpose({
}
.root {
outline: none;
&.dialog {
> .content {
position: fixed;

View file

@ -158,6 +158,7 @@ function onContextmenu(ev: MouseEvent) {
flex-direction: column;
contain: content;
border-radius: var(--radius);
margin: auto;
--root-margin: 24px;

View file

@ -3,59 +3,64 @@
ref="modal"
:prefer-type="'dialog'"
@click="onBgClick"
@keyup.esc="$emit('close')"
@closed="$emit('closed')"
>
<div
ref="rootEl"
class="ebkgoccj"
:style="{
width: `${width}px`,
height: scroll
? height
? `${height}px`
: null
: height
? `min(${height}px, 100%)`
: '100%',
}"
@keydown="onKeydown"
>
<div ref="headerEl" class="header">
<button
v-if="withOkButton"
class="_button"
@click="$emit('close')"
>
<i class="ph-x ph-bold ph-lg"></i>
</button>
<span class="title">
<slot name="header"></slot>
</span>
<button
v-if="!withOkButton"
class="_button"
@click="$emit('close')"
>
<i class="ph-x ph-bold ph-lg"></i>
</button>
<button
v-if="withOkButton"
class="_button"
:disabled="okButtonDisabled"
@click="$emit('ok')"
>
<i class="ph-check ph-bold ph-lg"></i>
</button>
<FocusTrap v-model:active="isActive">
<div
ref="rootEl"
class="ebkgoccj"
:style="{
width: `${width}px`,
height: scroll
? height
? `${height}px`
: null
: height
? `min(${height}px, 100%)`
: '100%',
}"
@keydown="onKeydown"
tabindex="-1"
>
<div ref="headerEl" class="header">
<button
v-if="withOkButton"
class="_button"
@click="$emit('close')"
>
<i class="ph-x ph-bold ph-lg"></i>
</button>
<span class="title">
<slot name="header"></slot>
</span>
<button
v-if="!withOkButton"
class="_button"
@click="$emit('close')"
>
<i class="ph-x ph-bold ph-lg"></i>
</button>
<button
v-if="withOkButton"
class="_button"
:disabled="okButtonDisabled"
@click="$emit('ok')"
>
<i class="ph-check ph-bold ph-lg"></i>
</button>
</div>
<div class="body">
<slot :width="bodyWidth" :height="bodyHeight"></slot>
</div>
</div>
<div class="body">
<slot :width="bodyWidth" :height="bodyHeight"></slot>
</div>
</div>
</FocusTrap>
</MkModal>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted } from "vue";
import { FocusTrap } from 'focus-trap-vue';
import MkModal from "./MkModal.vue";
const props = withDefaults(

View file

@ -84,6 +84,7 @@
:detailedView="detailedView"
:parentId="appearNote.parentId"
@push="(e) => router.push(notePage(e))"
@focusfooter="footerEl.focus()"
></MkSubNoteContent>
<div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini />
@ -117,7 +118,7 @@
<MkTime :time="appearNote.createdAt" mode="absolute" />
</MkA>
</div>
<footer ref="el" class="footer" @click.stop>
<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
<XReactionsViewer
v-if="enableEmojiReactions"
ref="reactionsViewer"
@ -278,6 +279,7 @@ const isRenote =
note.poll == null;
const el = ref<HTMLElement>();
const footerEl = ref<HTMLElement>();
const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
@ -298,8 +300,8 @@ const keymap = {
r: () => reply(true),
"e|a|plus": () => react(true),
q: () => renoteButton.value.renote(true),
"up|k|shift+tab": focusBefore,
"down|j|tab": focusAfter,
"up|k": focusBefore,
"down|j": focusAfter,
esc: blur,
"m|o": () => menu(true),
s: () => showContent.value !== showContent.value,

View file

@ -1,6 +1,6 @@
<template>
<div v-size="{ min: [350, 500] }" class="fefdfafb">
<MkAvatar class="avatar" :user="$i" />
<MkAvatar class="avatar" :user="$i" disableLink />
<div class="main">
<div class="header">
<MkUserName :user="$i" />

View file

@ -26,6 +26,7 @@
:note="note"
:parentId="appearNote.parentId"
:conversation="conversation"
@focusfooter="footerEl.focus()"
/>
<div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini />
@ -46,7 +47,7 @@
</div>
</div>
</div>
<footer class="footer" @click.stop>
<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
<XReactionsViewer
v-if="enableEmojiReactions"
ref="reactionsViewer"
@ -212,6 +213,7 @@ const isRenote =
note.poll == null;
const el = ref<HTMLElement>();
const footerEl = ref<HTMLElement>();
const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();

View file

@ -89,7 +89,7 @@
/>
</div>
</div>
<div class="tail">
<div class="tail" :class="{ collapsed }">
<header>
<span v-if="notification.type === 'pollEnded'">{{
i18n.ts._notification.pollEnded
@ -112,11 +112,11 @@
v-if="notification.type === 'reaction'"
class="text"
:to="notePage(notification.note)"
:title="getNoteSummary(notification.note)"
:title="summary"
>
<i class="ph-quotes ph-fill ph-lg"></i>
<Mfm
:text="getNoteSummary(notification.note)"
:text="summary"
:plain="true"
:nowrap="!full"
:custom-emojis="notification.note.emojis"
@ -142,10 +142,10 @@
v-if="notification.type === 'reply'"
class="text"
:to="notePage(notification.note)"
:title="getNoteSummary(notification.note)"
:title="summary"
>
<Mfm
:text="getNoteSummary(notification.note)"
:text="summary"
:plain="true"
:nowrap="!full"
:custom-emojis="notification.note.emojis"
@ -155,10 +155,10 @@
v-if="notification.type === 'mention'"
class="text"
:to="notePage(notification.note)"
:title="getNoteSummary(notification.note)"
:title="summary"
>
<Mfm
:text="getNoteSummary(notification.note)"
:text="summary"
:plain="true"
:nowrap="!full"
:custom-emojis="notification.note.emojis"
@ -168,10 +168,10 @@
v-if="notification.type === 'quote'"
class="text"
:to="notePage(notification.note)"
:title="getNoteSummary(notification.note)"
:title="summary"
>
<Mfm
:text="getNoteSummary(notification.note)"
:text="summary"
:plain="true"
:nowrap="!full"
:custom-emojis="notification.note.emojis"
@ -181,11 +181,11 @@
v-if="notification.type === 'pollVote'"
class="text"
:to="notePage(notification.note)"
:title="getNoteSummary(notification.note)"
:title="summary"
>
<i class="ph-quotes ph-fill ph-lg"></i>
<Mfm
:text="getNoteSummary(notification.note)"
:text="summary"
:plain="true"
:nowrap="!full"
:custom-emojis="notification.note.emojis"
@ -196,11 +196,11 @@
v-if="notification.type === 'pollEnded'"
class="text"
:to="notePage(notification.note)"
:title="getNoteSummary(notification.note)"
:title="summary"
>
<i class="ph-quotes ph-fill ph-lg"></i>
<Mfm
:text="getNoteSummary(notification.note)"
:text="summary"
:plain="true"
:nowrap="!full"
:custom-emojis="notification.note.emojis"
@ -264,6 +264,7 @@
<span v-if="notification.type === 'app'" class="text">
<Mfm :text="notification.body" :nowrap="!full" />
</span>
<xShowMoreButton v-if="isLong" v-model="collapsed"></xShowMoreButton>
</div>
</div>
</template>
@ -274,6 +275,7 @@ import * as misskey from "calckey-js";
import XReactionIcon from "@/components/MkReactionIcon.vue";
import MkFollowButton from "@/components/MkFollowButton.vue";
import XReactionTooltip from "@/components/MkReactionTooltip.vue";
import XShowMoreButton from "./MkShowMoreButton.vue";
import { getNoteSummary } from "@/scripts/get-note-summary";
import { notePage } from "@/filters/note";
import { userPage } from "@/filters/user";
@ -299,12 +301,19 @@ const props = withDefaults(
const elRef = ref<HTMLElement>(null);
const reactionRef = ref(null);
const summary = getNoteSummary(props.notification.note);
const showEmojiReactions =
defaultStore.state.enableEmojiReactions ||
defaultStore.state.showEmojisInReactionNotifications;
const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReaction)
? instance.defaultReaction
: "⭐";
const isLong = (summary.split("\n").length > 3 || summary.length > 200);
const collapsed = $ref(isLong);
let readObserver: IntersectionObserver | undefined;
let connection;
@ -486,6 +495,7 @@ useTooltip(reactionRef, (showing) => {
}
> .tail {
position: relative;
flex: 1;
min-width: 0;
@ -526,6 +536,17 @@ useTooltip(reactionRef, (showing) => {
margin-left: 4px;
}
}
&.collapsed > .text {
display: block;
position: relative;
max-height: calc(4em + 50px);
overflow: hidden;
mask: linear-gradient(black calc(100% - 64px), transparent);
-webkit-mask: linear-gradient(
black calc(100% - 64px),
transparent
);
}
}
}
</style>

View file

@ -7,6 +7,8 @@
:transparent-bg="true"
@click="modal.close()"
@closed="emit('closed')"
tabindex="-1"
v-focus
>
<MkMenu
:items="items"

View file

@ -55,7 +55,7 @@
:class="{ active: showPreview }"
@click="showPreview = !showPreview"
>
<i class="ph-file-code ph-bold ph-lg"></i>
<i class="ph-binoculars ph-bold ph-lg"></i>
</button>
<button
class="submit _buttonGradate"

View file

@ -154,22 +154,22 @@ export default defineComponent({
? i18n.ts.unmarkAsSensitive
: i18n.ts.markAsSensitive,
icon: file.isSensitive
? "ph-eye-slash ph-bold ph-lg"
: "ph-eye ph-bold ph-lg",
? "ph-eye ph-bold ph-lg"
: "ph-eye-slash ph-bold ph-lg",
action: () => {
this.toggleSensitive(file);
},
},
{
text: i18n.ts.describeFile,
icon: "ph-cursor-text ph-bold ph-lg",
icon: "ph-subtitles ph-bold ph-lg",
action: () => {
this.describe(file);
},
},
{
text: i18n.ts.attachCancel,
icon: "ph-circle-wavy-warning ph-bold ph-lg",
icon: "ph-x ph-bold ph-lg",
action: () => {
this.detachMedia(file.id);
},
@ -198,7 +198,6 @@ export default defineComponent({
height: 64px;
margin-right: 4px;
border-radius: 4px;
overflow: hidden;
cursor: move;
&:hover > .remove {

View file

@ -0,0 +1,68 @@
<template>
<button
v-if="modelValue"
class="fade _button"
@click.stop="toggle"
>
<span>{{ i18n.ts.showMore }}</span>
</button>
<button
v-if="!modelValue"
class="showLess _button"
@click.stop="toggle"
>
<span>{{ i18n.ts.showLess }}</span>
</button>
</template>
<script lang="ts" setup>
import { i18n } from "@/i18n";
const props = defineProps<{
modelValue: boolean;
}>();
const emit = defineEmits<{
(ev: "update:modelValue", v: boolean): void;
}>();
const toggle = () => {
emit("update:modelValue", !props.modelValue);
};
</script>
<style lang="scss" scoped>
.fade {
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
> span {
display: inline-block;
background: var(--panel);
padding: 0.4em 1em;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
}
&:hover {
> span {
background: var(--panelHighlight);
}
}
}
.showLess {
width: 100%;
margin-top: 1em;
position: sticky;
bottom: var(--stickyBottom);
> span {
display: inline-block;
background: var(--panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 0 7px 7px var(--bg);
}
}
</style>

View file

@ -35,7 +35,11 @@
class="content"
:class="{ collapsed, isLong, showContent: note.cw && !showContent }"
>
<div class="body">
<XCwButton ref="cwButton" v-if="note.cw && !showContent" v-model="showContent" :note="note" v-on:keydown="focusFooter" />
<div
class="body"
v-bind="{ 'aria-label': !showContent ? '' : null, 'tabindex': !showContent ? '-1' : null }"
>
<span v-if="note.deletedAt" style="opacity: 0.5"
>({{ i18n.ts.deleted }})</span
>
@ -96,34 +100,27 @@
<XNoteSimple :note="note.renote" />
</div>
</template>
<div
v-if="note.cw && !showContent"
tabindex="0"
v-on:focus="cwButton?.focus()"
></div>
</div>
<button
v-if="isLong && collapsed"
class="fade _button"
@click.stop="collapsed = false"
>
<span>{{ i18n.ts.showMore }}</span>
</button>
<button
v-if="isLong && !collapsed"
class="showLess _button"
@click.stop="collapsed = true"
>
<span>{{ i18n.ts.showLess }}</span>
</button>
<XCwButton v-if="note.cw" v-model="showContent" :note="note" />
<XShowMoreButton v-if="isLong" v-model="collapsed"></XShowMoreButton>
<XCwButton v-if="note.cw && showContent" v-model="showContent" :note="note" />
</div>
</div>
</template>
<script lang="ts" setup>
import {} from "vue";
import { ref } from "vue";
import * as misskey from "calckey-js";
import * as mfm from "mfm-js";
import XNoteSimple from "@/components/MkNoteSimple.vue";
import XMediaList from "@/components/MkMediaList.vue";
import XPoll from "@/components/MkPoll.vue";
import MkUrlPreview from "@/components/MkUrlPreview.vue";
import XShowMoreButton from "./MkShowMoreButton.vue";
import XCwButton from "@/components/MkCwButton.vue";
import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm";
import { i18n } from "@/i18n";
@ -138,19 +135,29 @@ const props = defineProps<{
const emit = defineEmits<{
(ev: "push", v): void;
(ev: "focusfooter"): void;
}>();
const cwButton = ref<HTMLElement>();
const isLong =
!props.detailedView &&
props.note.cw == null &&
props.note.text != null &&
(props.note.text.split("\n").length > 9 || props.note.text.length > 500);
const collapsed = $ref(props.note.cw == null && isLong);
const urls = props.note.text
? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5)
: null;
let showContent = $ref(false);
function focusFooter(ev) {
if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
emit("focusfooter");
}
}
</script>
<style lang="scss" scoped>
@ -242,6 +249,9 @@ let showContent = $ref(false);
margin-top: -50px;
padding-top: 50px;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
&.collapsed > .body {
box-sizing: border-box;
@ -264,43 +274,6 @@ let showContent = $ref(false);
top: 40px;
}
}
:deep(.fade) {
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
> span {
display: inline-block;
background: var(--panel);
padding: 0.4em 1em;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
}
&:hover {
> span {
background: var(--panelHighlight);
}
}
}
}
:deep(.showLess) {
width: 100%;
margin-top: 1em;
position: sticky;
bottom: var(--stickyBottom);
> span {
display: inline-block;
background: var(--panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 0 7px 7px var(--bg);
}
}
}
}

View file

@ -9,7 +9,6 @@
v-if="item.type === 'a'"
:href="item.href"
:target="item.target"
:tabindex="i"
class="_button item"
:class="{ danger: item.danger, active: item.active }"
>
@ -22,7 +21,6 @@
</a>
<button
v-else-if="item.type === 'button'"
:tabindex="i"
class="_button item"
:class="{ danger: item.danger, active: item.active }"
:disabled="item.active"
@ -38,7 +36,6 @@
<MkA
v-else
:to="item.to"
:tabindex="i"
class="_button item"
:class="{ danger: item.danger, active: item.active }"
>
@ -99,7 +96,7 @@ export default defineComponent({
font-size: 0.9em;
margin-bottom: 0.3rem;
&:hover {
&:hover, &:focus-visible {
text-decoration: none;
background: var(--panelHighlight);
}

View file

@ -46,7 +46,7 @@
/></MkA>
<p class="username"><MkAcct :user="user" /></p>
</div>
<div class="description">
<div class="description" :class="{ collapsed: isLong && collapsed }">
<Mfm
v-if="user.description"
:text="user.description"
@ -55,6 +55,32 @@
:custom-emojis="user.emojis"
/>
</div>
<XShowMoreButton v-if="isLong" v-model="collapsed"></XShowMoreButton>
<div v-if="user.fields.length > 0" class="fields">
<dl
v-for="(field, i) in user.fields"
:key="i"
class="field"
>
<dt class="name">
<Mfm
:text="field.name"
:plain="true"
:custom-emojis="user.emojis"
:colored="false"
/>
</dt>
<dd class="value">
<Mfm
:text="field.value"
:author="user"
:i="$i"
:custom-emojis="user.emojis"
:colored="false"
/>
</dd>
</dl>
</div>
<div class="status">
<div>
<p>{{ i18n.ts.notes }}</p>
@ -89,6 +115,7 @@ import * as Acct from "calckey-js/built/acct";
import type * as misskey from "calckey-js";
import MkFollowButton from "@/components/MkFollowButton.vue";
import { userPage } from "@/filters/user";
import XShowMoreButton from "./MkShowMoreButton.vue";
import * as os from "@/os";
import { $i } from "@/account";
import { i18n } from "@/i18n";
@ -110,9 +137,14 @@ let user = $ref<misskey.entities.UserDetailed | null>(null);
let top = $ref(0);
let left = $ref(0);
let isLong = $ref(false);
let collapsed = $ref(!isLong);
onMounted(() => {
if (typeof props.q === "object") {
user = props.q;
isLong = (user.description.split("\n").length > 9 || user.description.length > 400);
} else {
const query = props.q.startsWith("@")
? Acct.parse(props.q.substr(1))
@ -121,8 +153,10 @@ onMounted(() => {
os.api("users/show", query).then((res) => {
if (!props.showing) return;
user = res;
isLong = (user.description.split("\n").length > 9 || user.description.length > 400);
});
}
const rect = props.source.getBoundingClientRect();
const x =
@ -219,6 +253,88 @@ onMounted(() => {
padding: 0 16px;
font-size: 0.8em;
color: var(--fg);
&.collapsed {
position: relative;
max-height: calc(9em + 50px);
mask: linear-gradient(black calc(100% - 64px), transparent);
-webkit-mask: linear-gradient(
black calc(100% - 64px),
transparent
);
}
}
:deep(.fade) {
position: relative;
display: block;
width: 100%;
margin-top: -2.5em;
z-index: 2;
> span {
display: inline-block;
background: var(--panel);
padding: 0.4em 1em;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
}
&:hover {
> span {
background: var(--panelHighlight);
}
}
}
:deep(.showLess) {
width: 100%;
margin-top: 1em;
position: sticky;
bottom: var(--stickyBottom);
> span {
display: inline-block;
background: var(--panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 0 7px 7px var(--bg);
}
}
> .fields {
padding: 0 16px;
font-size: .8em;
margin-top: 1em;
> .field {
display: flex;
padding: 0;
margin: 0;
align-items: center;
&:not(:last-child) {
margin-bottom: 8px;
}
:deep(span) {
white-space: nowrap !important;
}
> .name {
width: 30%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: bold;
text-align: center;
}
> .value {
width: 70%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin: 0;
}
}
}
> .status {
@ -237,6 +353,9 @@ onMounted(() => {
> span {
font-size: 1em;
color: var(--accent);
:global(span) {
white-space: nowrap;
}
}
}
}

View file

@ -46,6 +46,7 @@
:user="user"
class="avatar"
:show-indicator="true"
disableLink
/>
<div class="body">
<MkUserName :user="user" class="name" />
@ -73,6 +74,7 @@
:user="user"
class="avatar"
:show-indicator="true"
disableLink
/>
<div class="body">
<MkUserName :user="user" class="name" />

View file

@ -7,7 +7,7 @@
>
<div class="beaffaef">
<div v-for="u in users" :key="u.id" class="user">
<MkAvatar class="avatar" :user="u" />
<MkAvatar class="avatar" :user="u" disableLink />
<MkUserName class="name" :user="u" :nowrap="true" />
</div>
<div v-if="users.length < count" class="omitted">

View file

@ -1,7 +1,7 @@
<template>
<div class="vjoppmmu">
<template v-if="edit">
<header>
<header tabindex="-1" v-focus>
<MkSelect
v-model="widgetAdderSelected"
style="margin-bottom: var(--margin)"

View file

@ -1,6 +1,6 @@
<template>
<div class="dwzlatin" :class="{ opened }">
<div class="header _button" @click="toggle">
<button class="header _button" @click="toggle">
<span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot name="label"></slot></span>
<span class="right">
@ -8,7 +8,7 @@
<i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i>
<i v-else class="ph-caret-down ph-bold ph-lg icon"></i>
</span>
</div>
</button>
<KeepAlive>
<div v-if="openedAtLeastOnce" v-show="opened" class="body">
<MkSpacer :margin-min="14" :margin-max="22">

View file

@ -66,6 +66,9 @@ function toggle(): void {
&:hover {
border-color: var(--inputBorderHover) !important;
}
&:focus-within {
outline: auto;
}
&.checked {
background-color: var(--accentedBg) !important;

View file

@ -99,6 +99,9 @@ const toggle = () => {
border-color: var(--inputBorderHover) !important;
}
}
&:focus-within > .button {
outline: auto;
}
> .label {
margin-left: 12px;

View file

@ -19,6 +19,7 @@
class="avatar"
:user="$i"
:disable-preview="true"
disableLink
/>
</div>
<template v-if="metadata">
@ -33,6 +34,7 @@
:user="metadata.avatar"
:disable-preview="true"
:show-indicator="true"
disableLink
/>
<i
v-else-if="metadata.icon && !narrow"

View file

@ -5,6 +5,9 @@
:is="currentPageComponent"
:key="key"
v-bind="Object.fromEntries(currentPageProps)"
tabindex="-1"
v-focus
style="outline: none;"
/>
<template #fallback>

View file

@ -0,0 +1,3 @@
export default {
mounted: (el) => el.focus()
}

View file

@ -11,6 +11,7 @@ import anim from "./anim";
import clickAnime from "./click-anime";
import panel from "./panel";
import adaptiveBorder from "./adaptive-border";
import focus from "./focus";
export default function (app: App) {
app.directive("userPreview", userPreview);
@ -25,4 +26,5 @@ export default function (app: App) {
app.directive("click-anime", clickAnime);
app.directive("panel", panel);
app.directive("adaptive-border", adaptiveBorder);
app.directive("focus", focus);
}

View file

@ -76,23 +76,32 @@ export default {
ev.preventDefault();
});
function showTooltip() {
window.clearTimeout(self.showTimer);
window.clearTimeout(self.hideTimer);
self.showTimer = window.setTimeout(self.show, delay);
}
function hideTooltip() {
window.clearTimeout(self.showTimer);
window.clearTimeout(self.hideTimer);
self.hideTimer = window.setTimeout(self.close, delay);
}
el.addEventListener(
start,
() => {
window.clearTimeout(self.showTimer);
window.clearTimeout(self.hideTimer);
self.showTimer = window.setTimeout(self.show, delay);
},
start, showTooltip,
{ passive: true },
);
el.addEventListener(
"focusin", showTooltip,
{ passive: true },
);
el.addEventListener(
end,
() => {
window.clearTimeout(self.showTimer);
window.clearTimeout(self.hideTimer);
self.hideTimer = window.setTimeout(self.close, delay);
},
end, hideTooltip,
{ passive: true },
);
el.addEventListener(
"focusout", hideTooltip,
{ passive: true },
);

View file

@ -313,11 +313,7 @@ onUnmounted(() => {
font-weight: normal;
opacity: 0.7;
&:hover {
opacity: 1;
}
&.active {
&:hover, &:focus-visible, &.active {
opacity: 1;
}

View file

@ -1,7 +1,7 @@
<template>
<div class="_panel" :class="$style.root">
<MkSelect v-model="src" style="margin: 0 0 12px 0" small>
<option value="notes">Notes</option>
<option value="notes">Posts</option>
<option value="active-users">Active users</option>
<option value="ap-requests-inbox-received">
Fediverse Requests: inboxReceived

View file

@ -12,7 +12,7 @@
class="user"
:to="`/user-info/${user.id}`"
>
<MkAvatar :user="user" class="avatar" indicator />
<MkAvatar :user="user" class="avatar" indicator disableLink />
</MkA>
</div>
</Transition>

View file

@ -23,6 +23,7 @@
class="avatar"
:user="req.follower"
:show-indicator="true"
disableLink
/>
<div class="body">
<div class="name">

View file

@ -111,7 +111,7 @@
/>
</div>
</div>
<MkAd :prefer="['horizontal', 'horizontal-big']" />
<MkAd :prefer="['inline', 'inline-big']" />
<MkContainer
:max-height="300"
:foldable="true"

View file

@ -341,6 +341,54 @@
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.position }}</div>
<div class="content">
<p>{{ i18n.ts._mfm.positionDescription }}</p>
<div class="preview">
<Mfm :text="preview_position" />
<MkTextarea v-model="preview_position"
><span>MFM</span></MkTextarea
>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.scale }}</div>
<div class="content">
<p>{{ i18n.ts._mfm.scaleDescription }}</p>
<div class="preview">
<Mfm :text="preview_scale" />
<MkTextarea v-model="preview_scale"
><span>MFM</span></MkTextarea
>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.foreground }}</div>
<div class="content">
<p>{{ i18n.ts._mfm.foregroundDescription }}</p>
<div class="preview">
<Mfm :text="preview_fg" />
<MkTextarea v-model="preview_fg"
><span>MFM</span></MkTextarea
>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.background }}</div>
<div class="content">
<p>{{ i18n.ts._mfm.backgroundDescription }}</p>
<div class="preview">
<Mfm :text="preview_bg" />
<MkTextarea v-model="preview_bg"
><span>MFM</span></MkTextarea
>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.plain }}</div>
<div class="content">
@ -402,7 +450,11 @@ let preview_x4 = $ref("$[x4 🍮]");
let preview_blur = $ref(`$[blur ${i18n.ts._mfm.dummy}]`);
let preview_rainbow = $ref("$[rainbow 🍮] $[rainbow.speed=5s 🍮]");
let preview_sparkle = $ref("$[sparkle 🍮]");
let preview_rotate = $ref("$[rotate 🍮]");
let preview_rotate = $ref("$[rotate 🍮]\n$[rotate.deg=45 🍮]\n$[rotate.x,deg=45 Hello, world!]");
let preview_position = $ref("$[position.y=-1 Positioning]\n$[position.x=-1 Positioning]");
let preview_scale = $ref("$[scale.x=1.3 Scaling]\n$[scale.x=1.3,y=2 Scaling]\n$[scale.y=0.3 Tiny scaling]");
let preview_fg = $ref("$[fg.color=ff0000 Text color]");
let preview_bg = $ref("$[bg.color=ff0000 Background color]");
let preview_plain = $ref(
"<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>"
);

View file

@ -168,7 +168,7 @@
</template>
</div> -->
</div>
<MkAd :prefer="['horizontal', 'horizontal-big']" />
<MkAd :prefer="['inline', 'inline-big']" />
<MkContainer
:max-height="300"
:foldable="true"

View file

@ -6,14 +6,14 @@
{{ i18n.ts.addAccount }}</FormButton
>
<div
<button
v-for="account in accounts"
:key="account.id"
class="_panel _button lcjjdxlm"
@click="menu(account, $event)"
>
<div class="avatar">
<MkAvatar :user="account" class="avatar" />
<MkAvatar :user="account" class="avatar" disableLink />
</div>
<div class="body">
<div class="name">
@ -23,7 +23,7 @@
<MkAcct :user="account" />
</div>
</div>
</div>
</button>
</FormSuspense>
</div>
</template>
@ -158,6 +158,8 @@ definePageMetadata({
.lcjjdxlm {
display: flex;
padding: 16px;
width: 100%;
text-align: unset;
> .avatar {
display: block;

View file

@ -21,9 +21,19 @@
<template #icon
><i class="ph-upload-simple ph-bold ph-lg"></i
></template>
<FormSwitch v-model="signatureCheck" class="_formBlock">
<!-- <FormSwitch v-model="signatureCheck" class="_formBlock">
Mastodon import? (not Akkoma!)
</FormSwitch>
</FormSwitch> -->
<FormRadios v-model="importType" class="_formBlock">
<option value="calckey">Calckey/Misskey</option>
<option value="mastodon">Mastodon</option>
<!-- <option :disabled="true" value="akkoma">
Pleroma/Akkoma (soon)
</option>
<option :disabled="true" value="twitter">
Twitter (soon)
</option> -->
</FormRadios>
<MkButton
primary
:class="$style.button"
@ -177,13 +187,14 @@ import MkButton from "@/components/MkButton.vue";
import FormSection from "@/components/form/section.vue";
import FormFolder from "@/components/form/folder.vue";
import FormSwitch from "@/components/form/switch.vue";
import FormRadios from "@/components/form/radios.vue";
import * as os from "@/os";
import { selectFile } from "@/scripts/select-file";
import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata";
const excludeMutingUsers = ref(false);
const signatureCheck = ref(false);
const importType = ref("calckey");
const excludeInactiveUsers = ref(false);
const onExportSuccess = () => {
@ -215,7 +226,7 @@ const importPosts = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
os.api("i/import-posts", {
fileId: file.id,
signatureCheck: signatureCheck.value,
signatureCheck: importType.value === "mastodon" ? true : false,
})
.then(onImportSuccess)
.catch(onError);

View file

@ -35,5 +35,3 @@ definePageMetadata({
icon: "ph-user ph-bold ph-lg",
});
</script>
<style lang="scss" scoped></style>

View file

@ -0,0 +1,39 @@
<template>
<div>
{{ i18n.ts.processing }}
</div>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
import * as os from "@/os";
import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata";
import { useRouter } from "@/router";
const router = useRouter();
const props = defineProps<{
code: string;
}>();
onMounted(async () => {
await os.alert({
type: "info",
text: i18n.t("clickToFinishEmailVerification", { ok: i18n.ts.gotIt }),
});
await os.api("verify-email", {
code: props.code,
});
router.push("/");
});
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: "Verify email",
icon: "ph-user ph-bold ph-lg",
});
</script>

View file

@ -308,6 +308,7 @@ function showMenu(ev) {
height: 32px;
border-radius: 8px;
font-size: 18px;
z-index: 2;
}
> .fg {

View file

@ -283,6 +283,10 @@ export const routes = [
path: "/signup-complete/:code",
component: page(() => import("./pages/signup-complete.vue")),
},
{
path: "/verify-email/:code",
component: page(() => import("./pages/verify-email.vue")),
},
{
path: "/announcements",
component: page(() => import("./pages/announcements.vue")),

View file

@ -204,10 +204,6 @@ hr {
pointer-events: none;
}
&:focus-visible {
outline: none;
}
&:disabled {
opacity: 0.5;
cursor: default;

View file

@ -18,6 +18,7 @@
<MkAvatar
:user="$i"
class="icon"
disableLink
/><!-- <MkAcct class="text" :user="$i"/> -->
</button>
</div>

View file

@ -18,6 +18,7 @@
<MkAvatar
:user="$i"
class="icon"
disableLink
/><!-- <MkAcct class="text" :user="$i"/> -->
</button>
</div>
@ -334,6 +335,7 @@ function more(ev: MouseEvent) {
}
&:hover,
&:focus-within,
&.active {
&:before {
background: var(--accentLighten);
@ -398,8 +400,6 @@ function more(ev: MouseEvent) {
padding-left: 30px;
line-height: 2.85rem;
margin-bottom: 0.5rem;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 100%;
text-align: left;
@ -425,9 +425,12 @@ function more(ev: MouseEvent) {
> .text {
position: relative;
font-size: 0.9em;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
&:hover,
&:focus-within {
text-decoration: none;
color: var(--navHoverFg);
transition: all 0.4s ease;
@ -437,7 +440,8 @@ function more(ev: MouseEvent) {
color: var(--navActive);
}
&:hover,
&:hover,
&:focus-within,
&.active {
color: var(--accent);
transition: all 0.4s ease;
@ -528,6 +532,7 @@ function more(ev: MouseEvent) {
}
&:hover,
&:focus-within,
&.active {
&:before {
background: var(--accentLighten);
@ -613,6 +618,7 @@ function more(ev: MouseEvent) {
}
&:hover,
&:focus-within,
&.active {
text-decoration: none;
color: var(--accent);
@ -642,5 +648,12 @@ function more(ev: MouseEvent) {
}
}
}
.item {
outline: none;
&:focus-visible:before {
outline: auto;
}
}
}
</style>

View file

@ -83,6 +83,7 @@
<MkAvatar :user="$i" class="avatar" /><MkAcct
class="acct"
:user="$i"
disableLink
/>
</button>
<div class="post" @click="post">

View file

@ -5,7 +5,7 @@
class="item _button account"
@click="openAccountMenu"
>
<MkAvatar :user="$i" class="avatar" /><MkAcct
<MkAvatar :user="$i" class="avatar" disableLink /><MkAcct
class="text"
:user="$i"
/>
@ -299,6 +299,7 @@ function openInstanceMenu(ev: MouseEvent) {
width: 46px;
height: 46px;
padding: 0;
margin-inline: 0 !important;
}
}
@ -372,6 +373,7 @@ function openInstanceMenu(ev: MouseEvent) {
> i {
width: 32px;
justify-content: center;
}
> i,

View file

@ -227,6 +227,8 @@ onMounted(() => {
}
.gbhvwtnk {
display: flex;
justify-content: center;
$ui-font-size: 1em;
$widgets-hide-threshold: 1200px;

View file

@ -3,8 +3,8 @@
<svg :viewBox="`0 0 ${viewBoxX} ${viewBoxY}`">
<defs>
<linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
<stop offset="0%" stop-color="hsl(189, 43%, 73%)"></stop>
<stop offset="100%" stop-color="hsl(343, 76%, 68%)"></stop>
</linearGradient>
<mask
:id="cpuMaskId"
@ -42,8 +42,8 @@
<svg :viewBox="`0 0 ${viewBoxX} ${viewBoxY}`">
<defs>
<linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
<stop offset="0%" stop-color="hsl(189, 43%, 73%)"></stop>
<stop offset="100%" stop-color="hsl(343, 76%, 68%)"></stop>
</linearGradient>
<mask
:id="memMaskId"

View file

@ -3,16 +3,16 @@
<svg :viewBox="`0 0 ${viewBoxX} ${viewBoxY}`">
<polygon
:points="inPolygonPoints"
fill="#94a029"
fill="#f6c177"
fill-opacity="0.5"
/>
<polyline
:points="inPolylinePoints"
fill="none"
stroke="#94a029"
stroke="#f6c177"
stroke-width="1"
/>
<circle :cx="inHeadX" :cy="inHeadY" r="1.5" fill="#94a029" />
<circle :cx="inHeadX" :cy="inHeadY" r="1.5" fill="#f6c177" />
<text x="1" y="5">
NET rx
<tspan>{{ bytes(inRecent) }}</tspan>
@ -21,16 +21,16 @@
<svg :viewBox="`0 0 ${viewBoxX} ${viewBoxY}`">
<polygon
:points="outPolygonPoints"
fill="#ff9156"
fill="#31748f"
fill-opacity="0.5"
/>
<polyline
:points="outPolylinePoints"
fill="none"
stroke="#ff9156"
stroke="#31748f"
stroke-width="1"
/>
<circle :cx="outHeadX" :cy="outHeadY" r="1.5" fill="#ff9156" />
<circle :cx="outHeadX" :cy="outHeadY" r="1.5" fill="#31748f" />
<text x="1" y="5">
NET tx
<tspan>{{ bytes(outRecent) }}</tspan>

View file

@ -23,6 +23,9 @@
"@cody@mk.codingneko.com",
"@kate@blahaj.zone",
"@emtk@mkkey.net",
"@jovikowi@calckey.social",
"@padraig@calckey.social",
"@pancakes@cats.city",
"Interkosmos Link"
]
}

View file

@ -19,6 +19,12 @@ importers:
'@tensorflow/tfjs':
specifier: ^3.21.0
version: 3.21.0(seedrandom@3.0.5)
focus-trap:
specifier: ^7.2.0
version: 7.2.0
focus-trap-vue:
specifier: ^4.0.1
version: 4.0.1(focus-trap@7.2.0)(vue@3.2.45)
js-yaml:
specifier: 4.1.0
version: 4.1.0
@ -81,8 +87,8 @@ importers:
specifier: ^4.6.4
version: 4.10.2
'@calckey/megalodon':
specifier: 5.1.24
version: 5.1.24
specifier: 5.2.0
version: 5.2.0
'@discordapp/twemoji':
specifier: 14.0.2
version: 14.0.2
@ -227,6 +233,9 @@ importers:
koa-bodyparser:
specifier: 4.3.0
version: 4.3.0
koa-favicon:
specifier: 2.1.0
version: 2.1.0
koa-json-body:
specifier: 5.3.0
version: 5.3.0
@ -236,6 +245,9 @@ importers:
koa-mount:
specifier: 4.0.0
version: 4.0.0
koa-remove-trailing-slashes:
specifier: 2.0.3
version: 2.0.3
koa-send:
specifier: 5.0.1
version: 5.0.1
@ -1372,8 +1384,8 @@ packages:
'@bull-board/api': 4.10.2
dev: false
/@calckey/megalodon@5.1.24:
resolution: {integrity: sha512-VRd6x8MFQ2pMF0rnGF67/GVxgp/92CV7lg2XT1wnPAfQZ1NTsjwlDQX3HewEW3fSG/r7Nzh5WbIBXC8WMWKs9g==}
/@calckey/megalodon@5.2.0:
resolution: {integrity: sha512-9MEjzKJPyd7o5bHGGlNq4oE1tMt22GUJ8o8tZXcXSpXlrSDb2rSwumirM1KXUWTW8G6NGi1leCM59gOBGLko3w==}
engines: {node: '>=15.0.0'}
dependencies:
'@types/oauth': 0.9.1
@ -3797,14 +3809,12 @@ packages:
'@vue/shared': 3.2.45
estree-walker: 2.0.2
source-map: 0.6.1
dev: true
/@vue/compiler-dom@3.2.45:
resolution: {integrity: sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==}
dependencies:
'@vue/compiler-core': 3.2.45
'@vue/shared': 3.2.45
dev: true
/@vue/compiler-sfc@2.7.14:
resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==}
@ -3827,14 +3837,12 @@ packages:
magic-string: 0.25.9
postcss: 8.4.21
source-map: 0.6.1
dev: true
/@vue/compiler-ssr@3.2.45:
resolution: {integrity: sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==}
dependencies:
'@vue/compiler-dom': 3.2.45
'@vue/shared': 3.2.45
dev: true
/@vue/reactivity-transform@3.2.45:
resolution: {integrity: sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==}
@ -3844,20 +3852,17 @@ packages:
'@vue/shared': 3.2.45
estree-walker: 2.0.2
magic-string: 0.25.9
dev: true
/@vue/reactivity@3.2.45:
resolution: {integrity: sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==}
dependencies:
'@vue/shared': 3.2.45
dev: true
/@vue/runtime-core@3.2.45:
resolution: {integrity: sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==}
dependencies:
'@vue/reactivity': 3.2.45
'@vue/shared': 3.2.45
dev: true
/@vue/runtime-dom@3.2.45:
resolution: {integrity: sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==}
@ -3865,7 +3870,6 @@ packages:
'@vue/runtime-core': 3.2.45
'@vue/shared': 3.2.45
csstype: 2.6.21
dev: true
/@vue/server-renderer@3.2.45(vue@3.2.45):
resolution: {integrity: sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==}
@ -3875,11 +3879,9 @@ packages:
'@vue/compiler-ssr': 3.2.45
'@vue/shared': 3.2.45
vue: 3.2.45
dev: true
/@vue/shared@3.2.45:
resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==}
dev: true
/@webassemblyjs/ast@1.11.1:
resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==}
@ -5908,8 +5910,8 @@ packages:
requiresBuild: true
dev: false
/core-js@3.30.0:
resolution: {integrity: sha512-hQotSSARoNh1mYPi9O2YaWeiq/cEB95kOrFb4NCrO4RIFt1qqNpKsaE+vy/L3oiqvND5cThqXzUU3r9F7Efztg==}
/core-js@3.30.1:
resolution: {integrity: sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==}
requiresBuild: true
dev: true
@ -6068,7 +6070,6 @@ packages:
/csstype@2.6.21:
resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==}
dev: true
/csstype@3.1.1:
resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
@ -6973,7 +6974,6 @@ packages:
/estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
dev: true
/esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
@ -7439,6 +7439,22 @@ packages:
readable-stream: 2.3.7
dev: true
/focus-trap-vue@4.0.1(focus-trap@7.2.0)(vue@3.2.45):
resolution: {integrity: sha512-2iqOeoSvgq7Um6aL+255a/wXPskj6waLq2oKCa4gOnMORPo15JX7wN6J5bl1SMhMlTlkHXGSrQ9uJPJLPZDl5w==}
peerDependencies:
focus-trap: ^7.0.0
vue: ^3.0.0
dependencies:
focus-trap: 7.2.0
vue: 3.2.45
dev: false
/focus-trap@7.2.0:
resolution: {integrity: sha512-v4wY6HDDYvzkBy4735kW5BUEuw6Yz9ABqMYLuTNbzAFPcBOGiGHwwcNVMvUz4G0kgSYh13wa/7TG3XwTeT4O/A==}
dependencies:
tabbable: 6.1.1
dev: false
/follow-redirects@1.15.2(debug@4.3.4):
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'}
@ -9835,6 +9851,12 @@ packages:
koa-compose: 4.1.0
dev: false
/koa-favicon@2.1.0:
resolution: {integrity: sha512-LvukcooYjxKtnZq0RXdBup+JDhaHwLgnLlDHB/xvjwQEjbc4rbp/0WkmOzpOvaHujc+fIwPear0dpKX1V+dHVg==}
dependencies:
mz: 2.7.0
dev: false
/koa-json-body@5.3.0:
resolution: {integrity: sha512-M2P3zLOs2XiYCpJLGSTXOKij4u5vJ8pbAMXXarXQnwsx4DwDav9qn081tYI2RdZ79B159Pdk4bRfvwl/sazL8A==}
engines: {node: '>= 4.0.0'}
@ -9862,6 +9884,10 @@ packages:
- supports-color
dev: false
/koa-remove-trailing-slashes@2.0.3:
resolution: {integrity: sha512-NFFF9Sl1wxFo5h0I3OzrHDINdFPaqG+Hx19590F7PNOcmm7yYeFW71p4XicVuSovbcx75GWGb3fi6N6kI6E/3g==}
dev: false
/koa-router@10.1.1:
resolution: {integrity: sha512-z/OzxVjf5NyuNO3t9nJpx7e1oR3FSBAauiwXtMQu4ppcnuNZzTaQ4p21P8A6r2Es8uJJM339oc4oVW+qX7SqnQ==}
engines: {node: '>= 8.0.0'}
@ -10334,7 +10360,6 @@ packages:
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
dependencies:
sourcemap-codec: 1.4.8
dev: true
/mailcheck@1.1.1:
resolution: {integrity: sha512-3WjL8+ZDouZwKlyJBMp/4LeziLFXgleOdsYu87piGcMLqhBzCsy2QFdbtAwv757TFC/rtqd738fgJw1tFQCSgA==}
@ -13251,7 +13276,6 @@ packages:
/sourcemap-codec@1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
deprecated: Please use @jridgewell/sourcemap-codec instead
dev: true
/sparkles@1.0.1:
resolution: {integrity: sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==}
@ -13670,6 +13694,10 @@ packages:
resolution: {integrity: sha512-g9rPT3V1Q4WjWFZ/t5BdGC1mT/FpYnsLdBl+M5e6MlRkuE1RSR+R43wcY/3mKI59B9KEr+vxdWCuWNMD3oNHKA==}
dev: true
/tabbable@6.1.1:
resolution: {integrity: sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg==}
dev: false
/tapable@2.2.1:
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
engines: {node: '>=6'}
@ -14760,7 +14788,6 @@ packages:
'@vue/runtime-dom': 3.2.45
'@vue/server-renderer': 3.2.45(vue@3.2.45)
'@vue/shared': 3.2.45
dev: true
/vuedraggable@4.1.0(vue@3.2.45):
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
@ -15386,7 +15413,7 @@ packages:
name: plyr
version: 3.7.0
dependencies:
core-js: 3.30.0
core-js: 3.30.1
custom-event-polyfill: 1.0.7
loadjs: 4.2.0
rangetouch: 2.0.1