1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Merge branch 'main' into release

This commit is contained in:
xmflsct
2022-12-21 15:34:33 +01:00
57 changed files with 202 additions and 315 deletions

View File

@ -1,5 +1,3 @@
Enjoy toooting! This version includes following improvements and fixes: Enjoy toooting! This version includes following improvements and fixes:
- Align filter experience with v4.0 and above - Fixed wrongly update notification
- Supports enlarging user's avatar and banner - Fix some random crashes
- Fix iPad weird sizing (not optimisation)
- Experiment (!) support of Pleroma

View File

@ -1,5 +1,3 @@
toooting愉快此版本包括以下改进和修复 toooting愉快此版本包括以下改进和修复
- 改进过滤体验与v4.0以上版本一致 - 修复错误的升级通知
- 支持查看用户的头像和横幅图片 - 修复部分应用崩溃
- 修复iPad部分尺寸问题非优化
- 试验性支持Pleroma

View File

@ -1,6 +1,6 @@
{ {
"name": "tooot", "name": "tooot",
"version": "4.7.1", "version": "4.7.2",
"description": "tooot for Mastodon", "description": "tooot for Mastodon",
"author": "xmflsct <me@xmflsct.com>", "author": "xmflsct <me@xmflsct.com>",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",

View File

@ -44,7 +44,10 @@ const apiGeneral = async <T = unknown>({
...userAgent, ...userAgent,
...headers ...headers
}, },
...(body && { data: body }) ...(body &&
(body instanceof FormData
? (body as (FormData & { _parts: [][] }) | undefined)?._parts?.length
: Object.keys(body).length) && { data: body })
}) })
.then(response => { .then(response => {
let links: { let links: {

View File

@ -64,7 +64,7 @@ const apiInstance = async <T = unknown>({
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`
}) })
}, },
...(body && { data: body }), ...((body as (FormData & { _parts: [][] }) | undefined)?._parts.length && { data: body }),
...extras ...extras
}) })
.then(response => { .then(response => {

View File

@ -47,7 +47,10 @@ const apiTooot = async <T = unknown>({
...userAgent, ...userAgent,
...headers ...headers
}, },
...(body && { data: body }) ...(body &&
(body instanceof FormData
? (body as (FormData & { _parts: [][] }) | undefined)?._parts?.length
: Object.keys(body).length) && { data: body })
}) })
.then(response => { .then(response => {
return Promise.resolve({ return Promise.resolve({

View File

@ -52,6 +52,7 @@ const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({
> >
#{hashtag.name} #{hashtag.name}
</CustomText> </CustomText>
{hashtag.history?.length ? (
<View <View
style={{ flexDirection: 'row', alignItems: 'center', alignSelf: 'stretch' }} style={{ flexDirection: 'row', alignItems: 'center', alignSelf: 'stretch' }}
onLayout={({ onLayout={({
@ -61,13 +62,14 @@ const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({
}) => setHeight(height)} }) => setHeight(height)}
> >
<Sparkline <Sparkline
data={hashtag.history?.map(h => parseInt(h.uses)).reverse()} data={hashtag.history.map(h => parseInt(h.uses)).reverse()}
width={width} width={width}
height={height} height={height}
margin={children ? StyleConstants.Spacing.S : undefined} margin={children ? StyleConstants.Spacing.S : undefined}
/> />
{children} {children}
</View> </View>
) : null}
</Pressable> </Pressable>
) )
} }

View File

@ -218,7 +218,7 @@ const ParseHTML = React.memo(
if (children) { if (children) {
return ( return (
<ParseEmojis <ParseEmojis
content={children.toString()} content={children?.toString()}
emojis={emojis} emojis={emojis}
size={size} size={size}
adaptiveSize={adaptiveSize} adaptiveSize={adaptiveSize}

View File

@ -54,7 +54,7 @@ const TimelineTranslate = () => {
<CustomText fontStyle='S' style={{ color: colors.secondary }}>{` Source: ${ <CustomText fontStyle='S' style={{ color: colors.secondary }}>{` Source: ${
detected?.language detected?.language
}; Confidence: ${ }; Confidence: ${
detected?.confidence.toString().slice(0, 5) || 'null' detected?.confidence?.toString().slice(0, 5) || 'null'
}; Target: ${targetLanguage}`}</CustomText> }; Target: ${targetLanguage}`}</CustomText>
) : null ) : null
} }

View File

@ -7,6 +7,7 @@ import { SearchResult } from '@utils/queryHooks/search'
import { getSettingsBrowser } from '@utils/slices/settingsSlice' import { getSettingsBrowser } from '@utils/slices/settingsSlice'
import * as Linking from 'expo-linking' import * as Linking from 'expo-linking'
import * as WebBrowser from 'expo-web-browser' import * as WebBrowser from 'expo-web-browser'
import validUrl from 'valid-url'
export let loadingLink = false export let loadingLink = false
@ -86,19 +87,22 @@ const openLink = async (url: string, navigation?: any) => {
} }
loadingLink = false loadingLink = false
const validatedUrl = validUrl.isWebUri(url)
if (validatedUrl) {
switch (getSettingsBrowser(store.getState())) { switch (getSettingsBrowser(store.getState())) {
// Some links might end with an empty space at the end that triggers an error // Some links might end with an empty space at the end that triggers an error
case 'internal': case 'internal':
await WebBrowser.openBrowserAsync(encodeURI(url), { await WebBrowser.openBrowserAsync(validatedUrl, {
dismissButtonStyle: 'close', dismissButtonStyle: 'close',
enableBarCollapsing: true, enableBarCollapsing: true,
...(await browserPackage()) ...(await browserPackage())
}) })
break break
case 'external': case 'external':
await Linking.openURL(encodeURI(url)) await Linking.openURL(validatedUrl)
break break
} }
}
} }
export default openLink export default openLink

View File

@ -15,13 +15,13 @@
"action_false": "Bloqueja l'usuari", "action_false": "Bloqueja l'usuari",
"action_true": "Deixa de bloquejar l'usuari", "action_true": "Deixa de bloquejar l'usuari",
"alert": { "alert": {
"title": "" "title": "Vols bloquejar a @{{username}}?"
} }
}, },
"reports": { "reports": {
"action": "Denuncia i bloqueja l'usuari", "action": "Denuncia i bloqueja l'usuari",
"alert": { "alert": {
"title": "" "title": "Vols denunciar i bloquejar a @{{username}}?"
} }
} }
}, },

View File

@ -32,7 +32,7 @@
}, },
"update": "L'impuls ha sigut editat", "update": "L'impuls ha sigut editat",
"admin.sign_up": "{{name}} s'ha unit a la instància", "admin.sign_up": "{{name}} s'ha unit a la instància",
"admin.report": "" "admin.report": "{{name}} ha reportat:"
}, },
"actions": { "actions": {
"reply": { "reply": {
@ -100,7 +100,7 @@
"fullConversation": "Llegeix conversacions", "fullConversation": "Llegeix conversacions",
"translate": { "translate": {
"default": "Tradueix", "default": "Tradueix",
"succeed": "Traduït per {{provider}} amb {{source}}", "succeed": "Traduït per {{provider}} des de l'idioma {{source}}",
"failed": "Error al traduir", "failed": "Error al traduir",
"source_not_supported": "L'idioma de la publicació no està suportada", "source_not_supported": "L'idioma de la publicació no està suportada",
"target_not_supported": "Aquest idioma no està suportat" "target_not_supported": "Aquest idioma no està suportat"

View File

@ -2,7 +2,7 @@
"heading": { "heading": {
"left": { "left": {
"alert": { "alert": {
"title": "Voleu cancel·lar l'edició?", "title": "Vols cancel·lar l'edició?",
"buttons": { "buttons": {
"save": "Desa l'esborrany", "save": "Desa l'esborrany",
"delete": "Esborra l'esborrany" "delete": "Esborra l'esborrany"
@ -13,9 +13,9 @@
"button": { "button": {
"default": "Publica", "default": "Publica",
"conversation": "Envia un missatge directe", "conversation": "Envia un missatge directe",
"reply": "Publica la resposta", "reply": "Respon",
"deleteEdit": "Publicació", "deleteEdit": "Torna-ho a publicar",
"edit": "Publica l'edició", "edit": "Edita",
"share": "Publicació" "share": "Publicació"
}, },
"alert": { "alert": {

View File

@ -133,7 +133,7 @@
"listDelete": { "listDelete": {
"heading": "Esborra la llista", "heading": "Esborra la llista",
"confirm": { "confirm": {
"title": "Vol esborrar la llista \"{{list}}\"?", "title": "Vols esborrar la llista \"{{list}}\"?",
"message": "Aquesta acció no es pot desfer." "message": "Aquesta acció no es pot desfer."
} }
}, },
@ -254,13 +254,10 @@
"content_true": "Habilitat", "content_true": "Habilitat",
"content_false": "Deshabilitat" "content_false": "Deshabilitat"
}, },
"update": {
"title": "Actualitza a la última versió"
},
"logout": { "logout": {
"button": "Tanca la sessió", "button": "Tanca la sessió",
"alert": { "alert": {
"title": "Vol tancar la sessió?", "title": "Vols tancar la sessió?",
"message": "Després de tancar la sessió, hauràs de tornar a iniciar la sessió", "message": "Després de tancar la sessió, hauràs de tornar a iniciar la sessió",
"buttons": { "buttons": {
"logout": "Tanca la sessió" "logout": "Tanca la sessió"

View File

@ -254,9 +254,6 @@
"content_true": "", "content_true": "",
"content_false": "" "content_false": ""
}, },
"update": {
"title": ""
},
"logout": { "logout": {
"button": "", "button": "",
"alert": { "alert": {

View File

@ -15,13 +15,13 @@
"action_false": "Nutzer blockieren", "action_false": "Nutzer blockieren",
"action_true": "User entblocken", "action_true": "User entblocken",
"alert": { "alert": {
"title": "" "title": "{{username}} wirklich blockieren?"
} }
}, },
"reports": { "reports": {
"action": "Nutzer melden und blockieren", "action": "Nutzer melden und blockieren",
"alert": { "alert": {
"title": "" "title": "{{username}} wirklich blockieren und melden?"
} }
} }
}, },

View File

@ -254,9 +254,6 @@
"content_true": "Aktiviert", "content_true": "Aktiviert",
"content_false": "Deaktiviert" "content_false": "Deaktiviert"
}, },
"update": {
"title": "Auf neueste Version aktualisiert"
},
"logout": { "logout": {
"button": "Abmelden", "button": "Abmelden",
"alert": { "alert": {

View File

@ -254,9 +254,6 @@
"content_true": "Enabled", "content_true": "Enabled",
"content_false": "Disabled" "content_false": "Disabled"
}, },
"update": {
"title": "Update to latest version"
},
"logout": { "logout": {
"button": "Log out", "button": "Log out",
"alert": { "alert": {

View File

@ -1,6 +1,6 @@
{ {
"buttons": { "buttons": {
"OK": "OK", "OK": "De acuerdo",
"apply": "Aplicar", "apply": "Aplicar",
"cancel": "Cancelar", "cancel": "Cancelar",
"discard": "Descartar", "discard": "Descartar",

View File

@ -15,13 +15,13 @@
"action_false": "Bloquear usuario", "action_false": "Bloquear usuario",
"action_true": "Desbloquear usuario", "action_true": "Desbloquear usuario",
"alert": { "alert": {
"title": "" "title": "¿Quieres bloquear a @{{username}}?"
} }
}, },
"reports": { "reports": {
"action": "Reportar y bloquear usuario", "action": "Reportar y bloquear usuario",
"alert": { "alert": {
"title": "" "title": "¿Quieres denunciar y bloquear a @{{username}}?"
} }
} }
}, },
@ -38,7 +38,7 @@
"block": { "block": {
"action": "Bloquear instancia {{instance}}", "action": "Bloquear instancia {{instance}}",
"alert": { "alert": {
"title": "¿Confirmar bloqueo de la instancia {{instance}}?", "title": "¿Quieres bloquear la instancia {{instance}}?",
"message": "Puedes silenciar o bloquear a un usuario.\n\nTras bloquear una instancia, todo su contenido junto con sus seguidores se eliminarán." "message": "Puedes silenciar o bloquear a un usuario.\n\nTras bloquear una instancia, todo su contenido junto con sus seguidores se eliminarán."
} }
} }
@ -59,14 +59,14 @@
"delete": { "delete": {
"action": "Eliminar toot", "action": "Eliminar toot",
"alert": { "alert": {
"title": "¿Confirmar eliminación?", "title": "¿Quieres eliminar?",
"message": "Todos los boosts y favoritos se eliminarán, incluidas todas las respuestas." "message": "Todos los boosts y favoritos se eliminarán, incluidas todas las respuestas."
} }
}, },
"deleteEdit": { "deleteEdit": {
"action": "Eliminar toot y volver a publicar", "action": "Eliminar toot y volver a publicar",
"alert": { "alert": {
"title": "¿Confirmar eliminación y volver a publicar?", "title": "¿Quieres eliminar y volver a publicar?",
"message": "Todos los boosts y favoritos se eliminarán, incluidas todas las respuestas." "message": "Todos los boosts y favoritos se eliminarán, incluidas todas las respuestas."
} }
}, },

View File

@ -32,7 +32,7 @@
}, },
"update": "El impulso ha sido editado", "update": "El impulso ha sido editado",
"admin.sign_up": "{{name}} se unió a la instancia", "admin.sign_up": "{{name}} se unió a la instancia",
"admin.report": "" "admin.report": "{{name}} reportó:"
}, },
"actions": { "actions": {
"reply": { "reply": {
@ -145,7 +145,7 @@
"refresh": "Actualizar" "refresh": "Actualizar"
}, },
"count": { "count": {
"voters_one": "{{count}} usuarios ha votado", "voters_one": "{{count}} usuario ha votado",
"voters_other": "{{count}} usuarios han votado", "voters_other": "{{count}} usuarios han votado",
"votes_one": "{{count}} voto", "votes_one": "{{count}} voto",
"votes_other": "{{count}} votos" "votes_other": "{{count}} votos"

View File

@ -2,7 +2,7 @@
"heading": { "heading": {
"left": { "left": {
"alert": { "alert": {
"title": "¿Cancelar edición?", "title": "¿Quieres cancelar la edición?",
"buttons": { "buttons": {
"save": "Guardar borrador", "save": "Guardar borrador",
"delete": "Eliminar borrador" "delete": "Eliminar borrador"
@ -13,9 +13,9 @@
"button": { "button": {
"default": "Toot", "default": "Toot",
"conversation": "Mensaje privado", "conversation": "Mensaje privado",
"reply": "Respuesta al toot", "reply": "Responde",
"deleteEdit": "Toot", "deleteEdit": "Vuelve a publicar",
"edit": "Edita el toot", "edit": "Edita",
"share": "Toot" "share": "Toot"
}, },
"alert": { "alert": {
@ -39,7 +39,7 @@
"placeholder": "Mensaje de aviso de spoiler" "placeholder": "Mensaje de aviso de spoiler"
}, },
"textInput": { "textInput": {
"placeholder": "Qué está pasando", "placeholder": "¿Qué está pasando?",
"keyboardImage": { "keyboardImage": {
"exceedMaximum": { "exceedMaximum": {
"title": "Número máximo de adjuntos alcanzado", "title": "Número máximo de adjuntos alcanzado",

View File

@ -11,7 +11,7 @@
"segments": { "segments": {
"federated": "Federado", "federated": "Federado",
"local": "Local", "local": "Local",
"trending": "En tendencia" "trending": "Tendencia"
} }
}, },
"notifications": { "notifications": {
@ -133,7 +133,7 @@
"listDelete": { "listDelete": {
"heading": "Borrar lista", "heading": "Borrar lista",
"confirm": { "confirm": {
"title": "¿Eliminar lista \"{{list}}\"?", "title": "¿Quieres eliminar la lista \"{{list}}\"?",
"message": "Esta acción no se podrá deshacer." "message": "Esta acción no se podrá deshacer."
} }
}, },
@ -254,9 +254,6 @@
"content_true": "Habilitado", "content_true": "Habilitado",
"content_false": "Deshabilitado" "content_false": "Deshabilitado"
}, },
"update": {
"title": "Actualizar a la última versión"
},
"logout": { "logout": {
"button": "Cerrar sesión", "button": "Cerrar sesión",
"alert": { "alert": {

View File

@ -254,9 +254,6 @@
"content_true": "Activé", "content_true": "Activé",
"content_false": "Désactivé" "content_false": "Désactivé"
}, },
"update": {
"title": "Mettre à jour vers la dernière version"
},
"logout": { "logout": {
"button": "Se déconnecter", "button": "Se déconnecter",
"alert": { "alert": {

View File

@ -254,9 +254,6 @@
"content_true": "Abilitato", "content_true": "Abilitato",
"content_false": "Disabilitato" "content_false": "Disabilitato"
}, },
"update": {
"title": "Aggiorna all'ultima versione"
},
"logout": { "logout": {
"button": "Esci dall'account", "button": "Esci dall'account",
"alert": { "alert": {

View File

@ -254,9 +254,6 @@
"content_true": "有効", "content_true": "有効",
"content_false": "無効" "content_false": "無効"
}, },
"update": {
"title": "最新バージョンへのアップデート"
},
"logout": { "logout": {
"button": "ログアウト", "button": "ログアウト",
"alert": { "alert": {

View File

@ -254,9 +254,6 @@
"content_true": "활성화됨", "content_true": "활성화됨",
"content_false": "비활성화됨" "content_false": "비활성화됨"
}, },
"update": {
"title": "최신 버전으로 업데이트"
},
"logout": { "logout": {
"button": "로그아웃", "button": "로그아웃",
"alert": { "alert": {

View File

@ -254,9 +254,6 @@
"content_true": "Ingeschakeld", "content_true": "Ingeschakeld",
"content_false": "Uitgeschakeld" "content_false": "Uitgeschakeld"
}, },
"update": {
"title": "Bijwerken naar de laatste versie"
},
"logout": { "logout": {
"button": "Uitloggen", "button": "Uitloggen",
"alert": { "alert": {

View File

@ -254,9 +254,6 @@
"content_true": "", "content_true": "",
"content_false": "" "content_false": ""
}, },
"update": {
"title": ""
},
"logout": { "logout": {
"button": "", "button": "",
"alert": { "alert": {

View File

@ -254,9 +254,6 @@
"content_true": "Habilitado", "content_true": "Habilitado",
"content_false": "Desabilitado" "content_false": "Desabilitado"
}, },
"update": {
"title": "Atualize para a versão mais recente"
},
"logout": { "logout": {
"button": "Sair", "button": "Sair",
"alert": { "alert": {

View File

@ -1 +1,3 @@
{} {
"frequentUsed": "Часто используемые"
}

View File

@ -1,27 +1,27 @@
{ {
"server": { "server": {
"textInput": { "textInput": {
"placeholder": "" "placeholder": "Домен инстанса"
}, },
"whitelisted": "", "whitelisted": "Это сервер может быть из белого списка, из которого tooot не может получить данные перед входом.",
"button": "", "button": "Войти",
"information": { "information": {
"name": "", "name": "Название",
"accounts": "", "accounts": "Пользователей",
"statuses": "", "statuses": "Постов",
"domains": "" "domains": ""
}, },
"disclaimer": { "disclaimer": {
"base": "" "base": "Вход в систему использует системный браузер, информация о вашем аккаунте не будет видна приложению."
}, },
"terms": { "terms": {
"base": "" "base": "Продолжая, вы соглашаетесь с <0>политикой конфиденциальности</0> и <1>условиями использования</1>."
} }
}, },
"update": { "update": {
"alert": { "alert": {
"title": "", "title": "Вход выполнен",
"message": "" "message": "Вы можете войти в другую учетную запись, сохранив существующую учетную запись"
} }
} }
} }

View File

@ -20,11 +20,11 @@
}, },
"shared": { "shared": {
"actioned": { "actioned": {
"pinned": "", "pinned": "Закрепить",
"favourite": "", "favourite": "",
"status": "", "status": "{{name}} только что запостил",
"follow": "", "follow": "{{name}} подписался (-лась) на вас",
"follow_request": "", "follow_request": "{{name}} хочет подписаться на вас",
"poll": "", "poll": "",
"reblog": { "reblog": {
"default": "", "default": "",
@ -92,7 +92,7 @@
"expandHint": "" "expandHint": ""
}, },
"filtered": { "filtered": {
"reveal": "", "reveal": "Все равно показать",
"match_v1": "", "match_v1": "",
"match_v2_one": "", "match_v2_one": "",
"match_v2_other": "" "match_v2_other": ""

View File

@ -1,17 +1,17 @@
{ {
"screenshot": { "screenshot": {
"title": "", "title": "Защита Персональных Данных",
"message": "" "message": "Пожалуйста, не раскрывайте личность других пользователей, таких как имя пользователя, аватар и т.д. Спасибо!"
}, },
"localCorrupt": { "localCorrupt": {
"message": "" "message": "Время входа истекло, повторите вход"
}, },
"pushError": { "pushError": {
"message": "", "message": "Ошибка службы Push",
"description": "" "description": "Пожалуйста, повторно включите push-уведомления в настройках"
}, },
"shareError": { "shareError": {
"imageNotSupported": "", "imageNotSupported": "Формат изображения {{type}} не поддерживается",
"videoNotSupported": "" "videoNotSupported": "Формат видео {{type}} не поддерживается"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"heading": "", "heading": "Поделиться...",
"content": { "content": {
"select_account": "" "select_account": "Выберите аккаунт"
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"content": { "content": {
"altText": { "altText": {
"heading": "" "heading": "Альтернативный текст"
} }
} }
} }

View File

@ -1,10 +1,10 @@
{ {
"heading": "", "heading": "Объявления",
"content": { "content": {
"published": "", "published": "Опубликовано <0 />",
"button": { "button": {
"read": "", "read": "Прочитано",
"unread": "" "unread": "Отметить как прочитанное"
} }
} }
} }

View File

@ -2,31 +2,31 @@
"heading": { "heading": {
"left": { "left": {
"alert": { "alert": {
"title": "", "title": "Отменить изменения?",
"buttons": { "buttons": {
"save": "", "save": "Сохранить черновик",
"delete": "" "delete": "Удалить черновик"
} }
} }
}, },
"right": { "right": {
"button": { "button": {
"default": "", "default": "Пост",
"conversation": "", "conversation": "Пост DM",
"reply": "", "reply": "Ответить",
"deleteEdit": "", "deleteEdit": "Пост",
"edit": "", "edit": "Пост",
"share": "" "share": "Пост"
}, },
"alert": { "alert": {
"default": { "default": {
"title": "", "title": "Не удалось опубликовать пост",
"button": "" "button": "Попробовать снова"
}, },
"removeReply": { "removeReply": {
"title": "", "title": "Пост для ответа не найдет",
"description": "", "description": "",
"confirm": "" "confirm": "Удалить ссылку"
} }
} }
} }
@ -34,7 +34,7 @@
"content": { "content": {
"root": { "root": {
"header": { "header": {
"postingAs": "", "postingAs": "Запостить как @{{acct}}@{{domain}}",
"spoilerInput": { "spoilerInput": {
"placeholder": "" "placeholder": ""
}, },
@ -50,7 +50,7 @@
}, },
"footer": { "footer": {
"attachments": { "attachments": {
"sensitive": "", "sensitive": "Пометить вложения как чувствительные",
"remove": { "remove": {
"accessibilityLabel": "" "accessibilityLabel": ""
}, },
@ -58,32 +58,32 @@
"accessibilityLabel": "" "accessibilityLabel": ""
}, },
"upload": { "upload": {
"accessibilityLabel": "" "accessibilityLabel": "Загрузить больше вложений"
} }
}, },
"emojis": { "emojis": {
"accessibilityHint": "" "accessibilityHint": "Нажмите, чтобы добавить эмодзи к посту"
}, },
"poll": { "poll": {
"option": { "option": {
"placeholder": { "placeholder": {
"accessibilityLabel": "", "accessibilityLabel": "Вариант опроса {{index}}",
"single": "", "single": "Единичный выбор",
"multiple": "" "multiple": "Множественный выбор"
} }
}, },
"quantity": { "quantity": {
"reduce": { "reduce": {
"accessibilityLabel": "", "accessibilityLabel": "Уменьшить количество вариантов ответа до {{amount}}",
"accessibilityHint": "" "accessibilityHint": ""
}, },
"increase": { "increase": {
"accessibilityLabel": "", "accessibilityLabel": "Уменьшить количество вариантов ответа до {{amount}}",
"accessibilityHint": "" "accessibilityHint": ""
} }
}, },
"multiple": { "multiple": {
"heading": "", "heading": "Тип выбора",
"options": { "options": {
"single": "", "single": "",
"multiple": "" "multiple": ""
@ -92,25 +92,25 @@
"expiration": { "expiration": {
"heading": "", "heading": "",
"options": { "options": {
"300": "", "300": "5 минут",
"1800": "", "1800": "30 минут",
"3600": "", "3600": "1 час",
"21600": "", "21600": "6 часов",
"86400": "", "86400": "1 день",
"259200": "", "259200": "3 дня",
"604800": "" "604800": "7 дней"
} }
} }
} }
}, },
"actions": { "actions": {
"attachment": { "attachment": {
"accessibilityLabel": "", "accessibilityLabel": "Загрузить вложение",
"accessibilityHint": "", "accessibilityHint": "",
"failed": { "failed": {
"alert": { "alert": {
"title": "", "title": "",
"button": "" "button": "Попробовать снова"
} }
} }
}, },
@ -120,7 +120,7 @@
}, },
"visibility": { "visibility": {
"accessibilityLabel": "", "accessibilityLabel": "",
"title": "", "title": "Видимость поста",
"options": { "options": {
"public": "", "public": "",
"unlisted": "", "unlisted": "",

View File

@ -4,7 +4,7 @@
"name": "", "name": "",
"options": { "options": {
"showBoosts": "", "showBoosts": "",
"showReplies": "" "showReplies": "Показать ответы"
} }
}, },
"public": { "public": {
@ -26,8 +26,8 @@
}, },
"notifications": { "notifications": {
"filters": { "filters": {
"accessibilityLabel": "", "accessibilityLabel": "Фильтр",
"accessibilityHint": "", "accessibilityHint": "Фильтр отображаемых типов уведомлений",
"title": "", "title": "",
"options": { "options": {
"follow": "", "follow": "",
@ -254,9 +254,6 @@
"content_true": "", "content_true": "",
"content_false": "" "content_false": ""
}, },
"update": {
"title": ""
},
"logout": { "logout": {
"button": "", "button": "",
"alert": { "alert": {

View File

@ -254,9 +254,6 @@
"content_true": "Aktiverad", "content_true": "Aktiverad",
"content_false": "Inaktiverad" "content_false": "Inaktiverad"
}, },
"update": {
"title": "Uppdatera till senaste versionen"
},
"logout": { "logout": {
"button": "Logga ut", "button": "Logga ut",
"alert": { "alert": {

View File

@ -254,9 +254,6 @@
"content_true": "Увімкнено", "content_true": "Увімкнено",
"content_false": "Вимкнено" "content_false": "Вимкнено"
}, },
"update": {
"title": "Оновити до останньої версії"
},
"logout": { "logout": {
"button": "Вийти", "button": "Вийти",
"alert": { "alert": {

View File

@ -254,9 +254,6 @@
"content_true": "Bật", "content_true": "Bật",
"content_false": "Tắt" "content_false": "Tắt"
}, },
"update": {
"title": "Cập nhật phiên bản mới"
},
"logout": { "logout": {
"button": "Đăng xuất", "button": "Đăng xuất",
"alert": { "alert": {

View File

@ -254,9 +254,6 @@
"content_true": "已启用", "content_true": "已启用",
"content_false": "未启用" "content_false": "未启用"
}, },
"update": {
"title": "更新至最新版本"
},
"logout": { "logout": {
"button": "退出当前账号", "button": "退出当前账号",
"alert": { "alert": {

View File

@ -254,9 +254,6 @@
"content_true": "啟用", "content_true": "啟用",
"content_false": "關閉" "content_false": "關閉"
}, },
"update": {
"title": "更新到最新版本"
},
"logout": { "logout": {
"button": "登出", "button": "登出",
"alert": { "alert": {

View File

@ -196,7 +196,7 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
rightOpenValue={-actionWidth} rightOpenValue={-actionWidth}
previewOpenValue={-actionWidth / 2} previewOpenValue={-actionWidth / 2}
ItemSeparatorComponent={ComponentSeparator} ItemSeparatorComponent={ComponentSeparator}
keyExtractor={item => item.timestamp.toString()} keyExtractor={item => item.timestamp?.toString()}
/> />
</PanGestureHandler> </PanGestureHandler>
<Modal <Modal

View File

@ -43,11 +43,11 @@ const composePost = async (
e => e && e.length && formData.append('poll[options][]', e) e => e && e.length && formData.append('poll[options][]', e)
) )
formData.append('poll[expires_in]', composeState.poll.expire) formData.append('poll[expires_in]', composeState.poll.expire)
formData.append('poll[multiple]', composeState.poll.multiple.toString()) formData.append('poll[multiple]', composeState.poll.multiple?.toString())
} }
if (composeState.attachments.uploads.filter(upload => upload.remote && upload.remote.id).length) { if (composeState.attachments.uploads.filter(upload => upload.remote && upload.remote.id).length) {
formData.append('sensitive', composeState.attachments.sensitive.toString()) formData.append('sensitive', composeState.attachments.sensitive?.toString())
composeState.attachments.uploads.forEach(e => formData.append('media_ids[]', e.remote!.id!)) composeState.attachments.uploads.forEach(e => formData.append('media_ids[]', e.remote!.id!))
} }

View File

@ -2,13 +2,11 @@ import GracefullyImage from '@components/GracefullyImage'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import { useAppDispatch } from '@root/store'
import { RootStackScreenProps, ScreenTabsStackParamList } from '@utils/navigation/navigators' import { RootStackScreenProps, ScreenTabsStackParamList } from '@utils/navigation/navigators'
import { getVersionUpdate, retrieveVersionLatest } from '@utils/slices/appSlice'
import { getPreviousTab } from '@utils/slices/contextsSlice' import { getPreviousTab } from '@utils/slices/contextsSlice'
import { getInstanceAccount, getInstanceActive } from '@utils/slices/instancesSlice' import { getInstanceAccount, getInstanceActive } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect, useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
import { Platform } from 'react-native' import { Platform } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import TabLocal from './Tabs/Local' import TabLocal from './Tabs/Local'
@ -55,17 +53,6 @@ const ScreenTabs = React.memo(
const previousTab = useSelector(getPreviousTab, () => true) const previousTab = useSelector(getPreviousTab, () => true)
const versionUpdate = useSelector(getVersionUpdate)
const dispatch = useAppDispatch()
useEffect(() => {
dispatch(retrieveVersionLatest())
}, [])
const tabMeOptions = useMemo(() => {
if (versionUpdate) {
return { tabBarBadge: 1 }
}
}, [versionUpdate])
return ( return (
<Tab.Navigator <Tab.Navigator
initialRouteName={instanceActive !== -1 ? previousTab : 'Tab-Me'} initialRouteName={instanceActive !== -1 ? previousTab : 'Tab-Me'}
@ -121,12 +108,7 @@ const ScreenTabs = React.memo(
<Tab.Screen name='Tab-Public' component={TabPublic} /> <Tab.Screen name='Tab-Public' component={TabPublic} />
<Tab.Screen name='Tab-Compose' component={composeComponent} listeners={composeListeners} /> <Tab.Screen name='Tab-Compose' component={composeComponent} listeners={composeListeners} />
<Tab.Screen name='Tab-Notifications' component={TabNotifications} /> <Tab.Screen name='Tab-Notifications' component={TabNotifications} />
<Tab.Screen <Tab.Screen name='Tab-Me' component={TabMe} listeners={meListeners} />
name='Tab-Me'
component={TabMe}
options={tabMeOptions}
listeners={meListeners}
/>
</Tab.Navigator> </Tab.Navigator>
) )
}, },

View File

@ -8,12 +8,7 @@ import { isDevelopment } from '@utils/checkEnvironment'
import { useAppsQuery } from '@utils/queryHooks/apps' import { useAppsQuery } from '@utils/queryHooks/apps'
import { useProfileQuery } from '@utils/queryHooks/profile' import { useProfileQuery } from '@utils/queryHooks/profile'
import { getExpoToken, retrieveExpoToken } from '@utils/slices/appSlice' import { getExpoToken, retrieveExpoToken } from '@utils/slices/appSlice'
import { import { PUSH_ADMIN, PUSH_DEFAULT, usePushFeatures } from '@utils/slices/instances/push/utils'
PUSH_ADMIN,
PUSH_DEFAULT,
setChannels,
usePushFeatures
} from '@utils/slices/instances/push/utils'
import { updateInstancePush } from '@utils/slices/instances/updatePush' import { updateInstancePush } from '@utils/slices/instances/updatePush'
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert' import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode' import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
@ -25,7 +20,7 @@ import * as Notifications from 'expo-notifications'
import * as WebBrowser from 'expo-web-browser' import * as WebBrowser from 'expo-web-browser'
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AppState, Linking, Platform, ScrollView, View } from 'react-native' import { AppState, Linking, ScrollView, View } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
const TabMePush: React.FC = () => { const TabMePush: React.FC = () => {
@ -50,12 +45,8 @@ const TabMePush: React.FC = () => {
setPushEnabled(permissions.granted) setPushEnabled(permissions.granted)
setPushCanAskAgain(permissions.canAskAgain) setPushCanAskAgain(permissions.canAskAgain)
layoutAnimation() layoutAnimation()
if (Platform.OS === 'android') {
await setChannels(instance)
dispatch(retrieveExpoToken()) dispatch(retrieveExpoToken())
} }
}
if (appsQuery.data?.vapid_key) { if (appsQuery.data?.vapid_key) {
checkPush() checkPush()

View File

@ -12,12 +12,8 @@ import accountReducer from '@screens/Tabs/Shared/Account/utils/reducer'
import { useProfileQuery } from '@utils/queryHooks/profile' import { useProfileQuery } from '@utils/queryHooks/profile'
import { getInstanceActive } from '@utils/slices/instancesSlice' import { getInstanceActive } from '@utils/slices/instancesSlice'
import React, { useReducer, useRef } from 'react' import React, { useReducer, useRef } from 'react'
import Animated, { import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
useAnimatedScrollHandler,
useSharedValue
} from 'react-native-reanimated'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import Update from './Root/Update'
const TabMeRoot: React.FC = () => { const TabMeRoot: React.FC = () => {
const instanceActive = useSelector(getInstanceActive) const instanceActive = useSelector(getInstanceActive)
@ -29,10 +25,7 @@ const TabMeRoot: React.FC = () => {
const scrollRef = useRef<Animated.ScrollView>(null) const scrollRef = useRef<Animated.ScrollView>(null)
useScrollToTop(scrollRef) useScrollToTop(scrollRef)
const [accountState, accountDispatch] = useReducer( const [accountState, accountDispatch] = useReducer(accountReducer, accountInitialState)
accountReducer,
accountInitialState
)
const scrollY = useSharedValue(0) const scrollY = useSharedValue(0)
const onScroll = useAnimatedScrollHandler(event => { const onScroll = useAnimatedScrollHandler(event => {
@ -41,22 +34,15 @@ const TabMeRoot: React.FC = () => {
return ( return (
<AccountContext.Provider value={{ accountState, accountDispatch }}> <AccountContext.Provider value={{ accountState, accountDispatch }}>
{instanceActive !== -1 && data ? ( {instanceActive !== -1 && data ? <AccountNav scrollY={scrollY} account={data} /> : null}
<AccountNav scrollY={scrollY} account={data} />
) : null}
<Animated.ScrollView <Animated.ScrollView
ref={scrollRef} ref={scrollRef}
keyboardShouldPersistTaps='handled' keyboardShouldPersistTaps='handled'
onScroll={onScroll} onScroll={onScroll}
scrollEventThrottle={16} scrollEventThrottle={16}
> >
{instanceActive !== -1 ? ( {instanceActive !== -1 ? <MyInfo account={data} /> : <ComponentInstance />}
<MyInfo account={data} />
) : (
<ComponentInstance />
)}
{instanceActive !== -1 ? <Collections /> : null} {instanceActive !== -1 ? <Collections /> : null}
<Update />
<Settings /> <Settings />
{instanceActive !== -1 ? <AccountInformationSwitch /> : null} {instanceActive !== -1 ? <AccountInformationSwitch /> : null}
{instanceActive !== -1 ? <Logout /> : null} {instanceActive !== -1 ? <Logout /> : null}

View File

@ -115,7 +115,11 @@ const Collections: React.FC = () => {
iconFront={instancePush ? 'Bell' : 'BellOff'} iconFront={instancePush ? 'Bell' : 'BellOff'}
iconBack='ChevronRight' iconBack='ChevronRight'
title={t('me.stacks.push.name')} title={t('me.stacks.push.name')}
content={t('me.root.push.content', { context: instancePush.global.toString() })} content={
typeof instancePush.global === 'boolean'
? t('me.root.push.content', { context: instancePush.global.toString() })
: undefined
}
onPress={() => navigation.navigate('Tab-Me-Push')} onPress={() => navigation.navigate('Tab-Me-Push')}
/> />
</MenuContainer> </MenuContainer>

View File

@ -1,32 +0,0 @@
import { MenuContainer, MenuRow } from '@components/Menu'
import { getVersionUpdate } from '@utils/slices/appSlice'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Linking, Platform } from 'react-native'
import { useSelector } from 'react-redux'
const Update: React.FC = () => {
const { t } = useTranslation('screenTabs')
const versionUpdate = useSelector(getVersionUpdate)
return versionUpdate ? (
<MenuContainer>
<MenuRow
iconFront='ChevronsUp'
iconBack='ExternalLink'
title={t('me.root.update.title')}
badge
onPress={() => {
if (Platform.OS === 'ios') {
Linking.openURL('itms-appss://itunes.apple.com/app/id1549772269')
} else {
Linking.openURL('https://tooot.app')
}
}}
/>
</MenuContainer>
) : null
}
export default Update

View File

@ -27,7 +27,7 @@ const TabMeSettingsLanguage: React.FC<TabMeStackScreenProps<'Tab-Me-Settings-Lan
// Update Android notification channel language // Update Android notification channel language
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
instances.forEach(setChannels) instances.forEach(instance => setChannels(instance, true))
} }
navigation.pop(1) navigation.pop(1)

View File

@ -1,11 +1,24 @@
import apiTooot from '@api/tooot'
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { RootState } from '@root/store' import { RootState } from '@root/store'
import { isDevelopment } from '@utils/checkEnvironment' import { isDevelopment } from '@utils/checkEnvironment'
import Constants from 'expo-constants'
import * as Notifications from 'expo-notifications' import * as Notifications from 'expo-notifications'
import { Platform } from 'react-native'
import { setChannels } from './instances/push/utils'
import { getInstance } from './instancesSlice'
export const retrieveExpoToken = createAsyncThunk('app/expoToken', async (): Promise<string> => { export const retrieveExpoToken = createAsyncThunk(
'app/expoToken',
async (_, { getState }): Promise<string> => {
const instance = getInstance(getState() as RootState)
const expoToken = getExpoToken(getState() as RootState)
if (Platform.OS === 'android') {
await setChannels(instance)
}
if (expoToken?.length) {
return expoToken
} else {
if (isDevelopment) { if (isDevelopment) {
return 'ExponentPushToken[DEVELOPMENT_1]' return 'ExponentPushToken[DEVELOPMENT_1]'
} }
@ -15,24 +28,16 @@ export const retrieveExpoToken = createAsyncThunk('app/expoToken', async (): Pro
applicationId: 'com.xmflsct.app.tooot' applicationId: 'com.xmflsct.app.tooot'
}) })
return res.data return res.data
}) }
export const retrieveVersionLatest = createAsyncThunk(
'app/versionUpdate',
async (): Promise<string> => {
const res = await apiTooot<{ latest: string }>({ method: 'get', url: 'version.json' })
return res.body.latest
} }
) )
export type AppState = { export type AppState = {
expoToken?: string expoToken?: string
versionUpdate: boolean
} }
export const appInitialState: AppState = { export const appInitialState: AppState = {
expoToken: undefined, expoToken: undefined
versionUpdate: false
} }
const appSlice = createSlice({ const appSlice = createSlice({
@ -40,22 +45,14 @@ const appSlice = createSlice({
initialState: appInitialState, initialState: appInitialState,
reducers: {}, reducers: {},
extraReducers: builder => { extraReducers: builder => {
builder builder.addCase(retrieveExpoToken.fulfilled, (state, action) => {
.addCase(retrieveExpoToken.fulfilled, (state, action) => {
if (action.payload) { if (action.payload) {
state.expoToken = action.payload state.expoToken = action.payload
} }
}) })
.addCase(retrieveVersionLatest.fulfilled, (state, action) => {
if (action.payload && Constants.expoConfig?.version) {
state.versionUpdate =
parseFloat(action.payload) > parseFloat(Constants.expoConfig.version)
}
})
} }
}) })
export const getExpoToken = (state: RootState) => state.app.expoToken export const getExpoToken = (state: RootState) => state.app.expoToken
export const getVersionUpdate = (state: RootState) => state.app.versionUpdate
export default appSlice.reducer export default appSlice.reducer

View File

@ -28,7 +28,7 @@ const subscribe = async ({
}) => { }) => {
return apiTooot({ return apiTooot({
method: 'post', method: 'post',
url: `/push/subscribe/${expoToken}/${instanceUrl}/${accountId}`, url: `push/subscribe/${expoToken}/${instanceUrl}/${accountId}`,
body: { accountFull, serverKey, auth } body: { accountFull, serverKey, auth }
}) })
} }
@ -97,7 +97,7 @@ const pushRegister = async (
}) })
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
setChannels(instance) setChannels(instance, true)
} }
return Promise.resolve(auth) return Promise.resolve(auth)

View File

@ -21,7 +21,7 @@ const pushUnregister = async (state: RootState, expoToken: string) => {
await apiTooot<{ endpoint: string; publicKey: string; auth: string }>({ await apiTooot<{ endpoint: string; publicKey: string; auth: string }>({
method: 'delete', method: 'delete',
url: `/push/unsubscribe/${expoToken}/${instance.url}/${instance.account.id}` url: `push/unsubscribe/${expoToken}/${instance.url}/${instance.account.id}`
}) })
if (Platform.OS === 'android') { if (Platform.OS === 'android') {

View File

@ -62,7 +62,7 @@ export const PUSH_ADMIN = (
} }
}) as { type: 'admin.sign_up' | 'admin.report'; permission: number }[] }) as { type: 'admin.sign_up' | 'admin.report'; permission: number }[]
export const setChannels = async (instance: InstanceLatest) => { export const setChannels = async (instance: InstanceLatest, reset: boolean | undefined = false) => {
const account = `@${instance.account.acct}@${instance.uri}` const account = `@${instance.account.acct}@${instance.uri}`
const deleteChannel = async (type: string) => const deleteChannel = async (type: string) =>
@ -82,6 +82,9 @@ export const setChannels = async (instance: InstanceLatest) => {
const profileQuery = await queryClient.fetchQuery(queryKey, queryFunctionProfile) const profileQuery = await queryClient.fetchQuery(queryKey, queryFunctionProfile)
const channelGroup = await Notifications.getNotificationChannelGroupAsync(account) const channelGroup = await Notifications.getNotificationChannelGroupAsync(account)
if (channelGroup && !reset) {
return
}
if (!channelGroup) { if (!channelGroup) {
await Notifications.setNotificationChannelGroupAsync(account, { name: account }) await Notifications.setNotificationChannelGroupAsync(account, { name: account })
} }

View File

@ -25,14 +25,14 @@ export const updateInstancePushDecode = createAsyncThunk(
await apiTooot({ await apiTooot({
method: 'put', method: 'put',
url: `/push/update-decode/${expoToken}/${instance.url}/${instance.account.id}`, url: `push/update-decode/${expoToken}/${instance.url}/${instance.account.id}`,
body: { body: {
auth: !disable ? null : instance.push.keys.auth auth: !disable ? null : instance.push.keys.auth
} }
}) })
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
setChannels(instance) setChannels(instance, true)
} }
return Promise.resolve({ disable }) return Promise.resolve({ disable })