Merge pull request #281 from tooot-app/main

Test updates
This commit is contained in:
xmflsct 2022-05-08 23:58:18 +02:00 committed by GitHub
commit dfb1e32fc8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
140 changed files with 4845 additions and 4806 deletions

View File

@ -19,17 +19,22 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 16
- name: -- Step 3 -- Use Expo action
- name: -- Step 3 -- Setup Java
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '11'
- name: -- Step 4 -- Use Expo action
uses: expo/expo-github-action@v6
with:
expo-version: 5.x
username: ${{ secrets.EXPO_USERNAME }}
token: ${{ secrets.EXPO_TOKEN }}
- name: -- Step 4 -- Install node dependencies
- name: -- Step 5 -- Install node dependencies
run: yarn install
- name: -- Step 5 -- Install ruby dependencies
- name: -- Step 6 -- Install ruby dependencies
run: bundle install
- name: -- Step 6 -- Run fastlane
- name: -- Step 7 -- Run fastlane
env:
DEVELOPER_DIR: /Applications/Xcode_13.3.1.app/Contents/Developer
ENVIRONMENT: ${{ steps.branch.outputs.branch }}

View File

@ -1,96 +0,0 @@
import React from 'react'
import {
toBeDisabled,
toHaveStyle,
toHaveTextContent
} from '@testing-library/jest-native'
import { cleanup, fireEvent, render } from '@testing-library/react-native/pure'
import Button from '@components/Button'
expect.extend({ toBeDisabled, toHaveStyle, toHaveTextContent })
describe('Testing component button', () => {
afterEach(cleanup)
describe('static button', () => {
it('with text only', () => {
const onPress = jest.fn()
const { getByTestId, toJSON } = render(
<Button type='text' content='Test Button' onPress={onPress} />
)
fireEvent.press(getByTestId('base'))
expect(onPress).toHaveBeenCalled()
expect(onPress).toHaveBeenCalledTimes(1)
expect(getByTestId('text')).toHaveTextContent('Test Button')
expect(toJSON()).toMatchSnapshot()
})
it('with icon only', () => {
const onPress = jest.fn()
const { getByTestId, toJSON } = render(
<Button type='icon' content='X' onPress={onPress} />
)
fireEvent.press(getByTestId('base'))
expect(onPress).toHaveBeenCalled()
expect(onPress).toHaveBeenCalledTimes(1)
expect(toJSON()).toMatchSnapshot()
})
it('loading state', () => {
const { getByTestId, toJSON } = render(
<Button type='text' content='test' onPress={jest.fn()} loading />
)
expect(getByTestId('base')).toBeDisabled()
expect(toJSON()).toMatchSnapshot()
})
it('disabled state', () => {
const { getByTestId, toJSON } = render(
<Button type='text' content='test' onPress={jest.fn()} disabled />
)
expect(getByTestId('base')).toBeDisabled()
expect(toJSON()).toMatchSnapshot()
})
it('apply custom styling', () => {
const { getByTestId, toJSON } = render(
<Button
type='text'
content='test'
onPress={jest.fn()}
style={{ backgroundColor: 'black' }}
/>
)
expect(getByTestId('base')).toHaveStyle({ backgroundColor: 'black' })
expect(toJSON()).toMatchSnapshot()
})
})
describe('dynamic button', () => {
it('from default to loading', () => {
const onPress = jest.fn()
const { getByTestId, rerender } = render(
<Button type='text' content='test' onPress={onPress} />
)
rerender(<Button type='text' content='test' onPress={onPress} loading />)
expect(getByTestId('base')).toBeDisabled()
})
it('from default to disabled', () => {
const onPress = jest.fn()
const { getByTestId, rerender } = render(
<Button type='text' content='test' onPress={onPress} />
)
rerender(<Button type='text' content='test' onPress={onPress} disabled />)
expect(getByTestId('base')).toBeDisabled()
})
})
})

View File

@ -1,15 +0,0 @@
import React from 'react'
import { cleanup, render } from '@testing-library/react-native/pure'
import MenuHeader from '@components/Menu/Header'
describe('Testing component menu header', () => {
afterEach(cleanup)
it('with text only', () => {
const { getByText, toJSON } = render(<MenuHeader heading='test' />)
getByText('test')
expect(toJSON()).toMatchSnapshot()
})
})

View File

@ -1,50 +0,0 @@
import React from 'react'
import { toBeDisabled } from '@testing-library/jest-native'
import { cleanup, fireEvent, render } from '@testing-library/react-native'
import MenuRow from '@components/Menu/Row'
expect.extend({ toBeDisabled })
describe('Testing component menu row', () => {
afterEach(cleanup)
it('with title only', () => {
const { getByText, toJSON } = render(<MenuRow title='test title' />)
getByText('test title')
expect(toJSON()).toMatchSnapshot()
})
it('with title and content', () => {
const { getByText, toJSON } = render(
<MenuRow title='test title' content='test content' />
)
getByText('test title')
getByText('test content')
expect(toJSON()).toMatchSnapshot()
})
it('on press event', () => {
const onPress = jest.fn()
const { getByTestId, toJSON } = render(
<MenuRow title='test' onPress={onPress} />
)
fireEvent.press(getByTestId('base'))
expect(onPress).toHaveBeenCalled()
expect(onPress).toHaveBeenCalledTimes(1)
expect(toJSON()).toMatchSnapshot()
})
it('loading state', () => {
const onPress = jest.fn()
const { getByTestId, toJSON } = render(
<MenuRow title='test' loading onPress={onPress} />
)
fireEvent.press(getByTestId('base'))
expect(onPress).toHaveBeenCalledTimes(0)
expect(getByTestId('base')).toBeDisabled()
expect(toJSON()).toMatchSnapshot()
})
})

View File

@ -1,30 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Testing component menu header with text only 1`] = `
<View
style={
Object {
"paddingBottom": 8,
"paddingLeft": 16,
"paddingRight": 16,
}
}
>
<Text
style={
Array [
Object {
"fontSize": 14,
"fontWeight": "600",
"lineHeight": 20,
},
Object {
"color": "rgb(135, 135, 135)",
},
]
}
>
test
</Text>
</View>
`;

View File

@ -1,302 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Testing component menu row loading state 1`] = `
<View
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"minHeight": 50,
}
}
testID="base"
>
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
"paddingLeft": 16,
"paddingRight": 16,
}
}
>
<View
style={
Object {
"alignItems": "center",
"flex": 2,
"flexDirection": "row",
}
}
>
<View
style={
Object {
"flex": 1,
}
}
>
<Text
numberOfLines={1}
style={
Array [
Object {
"fontSize": 16,
"lineHeight": 22,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
}
>
test
</Text>
</View>
</View>
</View>
</View>
`;
exports[`Testing component menu row on press event 1`] = `
<View
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"minHeight": 50,
}
}
testID="base"
>
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
"paddingLeft": 16,
"paddingRight": 16,
}
}
>
<View
style={
Object {
"alignItems": "center",
"flex": 2,
"flexDirection": "row",
}
}
>
<View
style={
Object {
"flex": 1,
}
}
>
<Text
numberOfLines={1}
style={
Array [
Object {
"fontSize": 16,
"lineHeight": 22,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
}
>
test
</Text>
</View>
</View>
</View>
</View>
`;
exports[`Testing component menu row with title and content 1`] = `
<View
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"minHeight": 50,
}
}
testID="base"
>
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
"paddingLeft": 16,
"paddingRight": 16,
}
}
>
<View
style={
Object {
"alignItems": "center",
"flex": 2,
"flexDirection": "row",
}
}
>
<View
style={
Object {
"flex": 1,
}
}
>
<Text
numberOfLines={1}
style={
Array [
Object {
"fontSize": 16,
"lineHeight": 22,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
}
>
test title
</Text>
</View>
</View>
<View
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"justifyContent": "flex-end",
"marginLeft": 16,
}
}
>
<Text
numberOfLines={1}
style={
Array [
Object {
"fontSize": 16,
"lineHeight": 22,
},
Object {
"color": "rgb(135, 135, 135)",
"opacity": 1,
},
]
}
>
test content
</Text>
</View>
</View>
</View>
`;
exports[`Testing component menu row with title only 1`] = `
<View
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"minHeight": 50,
}
}
testID="base"
>
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
"paddingLeft": 16,
"paddingRight": 16,
}
}
>
<View
style={
Object {
"alignItems": "center",
"flex": 2,
"flexDirection": "row",
}
}
>
<View
style={
Object {
"flex": 1,
}
}
>
<Text
numberOfLines={1}
style={
Array [
Object {
"fontSize": 16,
"lineHeight": 22,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
}
>
test title
</Text>
</View>
</View>
</View>
</View>
`;

View File

@ -1,59 +0,0 @@
import React from 'react'
import {
toBeDisabled,
toContainElement,
toHaveStyle,
toHaveTextContent
} from '@testing-library/jest-native'
import { cleanup, render } from '@testing-library/react-native/pure'
import Card from '@components/Timelines/Timeline/Shared/Card'
expect.extend({
toBeDisabled,
toContainElement,
toHaveStyle,
toHaveTextContent
})
describe('Testing component timeline card', () => {
afterEach(cleanup)
it('with text only', () => {
const { getByTestId, queryByTestId, toJSON } = render(
<Card
card={{
url: 'http://example.com',
title: 'Title'
}}
/>
)
expect(queryByTestId('image')).toBeNull()
expect(getByTestId('base')).toContainElement(getByTestId('title'))
expect(queryByTestId('description')).toBeNull()
expect(getByTestId('title')).toHaveTextContent('Title')
expect(toJSON()).toMatchSnapshot()
})
it('with text and description', () => {
const { getByTestId, queryByTestId, toJSON } = render(
<Card
card={{
url: 'http://example.com',
title: 'Title',
description: 'Description'
}}
/>
)
expect(queryByTestId('image')).toBeNull()
expect(getByTestId('base')).toContainElement(getByTestId('title'))
expect(getByTestId('base')).toContainElement(getByTestId('description'))
expect(getByTestId('title')).toHaveTextContent('Title')
expect(getByTestId('description')).toHaveTextContent('Description')
expect(toJSON()).toMatchSnapshot()
})
})

View File

@ -1,155 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Testing component timeline card with text and description 1`] = `
<View
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Array [
Object {
"borderRadius": 6,
"borderWidth": 0.5,
"flex": 1,
"flexDirection": "row",
"height": 104,
"marginTop": 16,
},
Object {
"borderColor": "rgba(18, 18, 18, 0.3)",
},
]
}
testID="base"
>
<View
style={
Object {
"flex": 1,
"padding": 8,
}
}
>
<Text
numberOfLines={2}
style={
Array [
Object {
"fontWeight": "600",
"marginBottom": 4,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
}
testID="title"
>
Title
</Text>
<Text
numberOfLines={1}
style={
Array [
Object {
"marginBottom": 4,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
}
testID="description"
>
Description
</Text>
<Text
numberOfLines={1}
style={
Object {
"color": "rgb(135, 135, 135)",
}
}
>
http://example.com
</Text>
</View>
</View>
`;
exports[`Testing component timeline card with text only 1`] = `
<View
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Array [
Object {
"borderRadius": 6,
"borderWidth": 0.5,
"flex": 1,
"flexDirection": "row",
"height": 104,
"marginTop": 16,
},
Object {
"borderColor": "rgba(18, 18, 18, 0.3)",
},
]
}
testID="base"
>
<View
style={
Object {
"flex": 1,
"padding": 8,
}
}
>
<Text
numberOfLines={2}
style={
Array [
Object {
"fontWeight": "600",
"marginBottom": 4,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
}
testID="title"
>
Title
</Text>
<Text
numberOfLines={1}
style={
Object {
"color": "rgb(135, 135, 135)",
}
}
>
http://example.com
</Text>
</View>
</View>
`;

View File

@ -1,474 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Testing component button static button apply custom styling 1`] = `
<View>
<View
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Array [
Object {
"alignItems": "center",
"borderRadius": 100,
"flexDirection": "row",
"justifyContent": "center",
},
Object {
"backgroundColor": "rgb(250, 250, 250)",
"borderColor": "rgb(18, 18, 18)",
"borderWidth": 1,
"paddingHorizontal": 16,
"paddingVertical": 8,
},
Object {
"backgroundColor": "black",
},
]
}
testID="base"
>
<Text
style={
Object {
"color": "rgb(18, 18, 18)",
"fontSize": 16,
"fontWeight": undefined,
"opacity": 1,
}
}
testID="text"
>
test
</Text>
</View>
</View>
`;
exports[`Testing component button static button disabled state 1`] = `
<View>
<View
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Array [
Object {
"alignItems": "center",
"borderRadius": 100,
"flexDirection": "row",
"justifyContent": "center",
},
Object {
"backgroundColor": "rgb(250, 250, 250)",
"borderColor": "rgb(135, 135, 135)",
"borderWidth": 1,
"paddingHorizontal": 16,
"paddingVertical": 8,
},
undefined,
]
}
testID="base"
>
<Text
style={
Object {
"color": "rgb(135, 135, 135)",
"fontSize": 16,
"fontWeight": undefined,
"opacity": 1,
}
}
testID="text"
>
test
</Text>
</View>
</View>
`;
exports[`Testing component button static button loading state 1`] = `
<View>
<View
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Array [
Object {
"alignItems": "center",
"borderRadius": 100,
"flexDirection": "row",
"justifyContent": "center",
},
Object {
"backgroundColor": "rgb(250, 250, 250)",
"borderColor": "rgb(135, 135, 135)",
"borderWidth": 1,
"paddingHorizontal": 16,
"paddingVertical": 8,
},
undefined,
]
}
testID="base"
>
<Text
style={
Object {
"color": "rgb(18, 18, 18)",
"fontSize": 16,
"fontWeight": undefined,
"opacity": 0,
}
}
testID="text"
>
test
</Text>
<View
style={
Object {
"position": "absolute",
}
}
>
<View
style={
Object {
"alignItems": "center",
"height": 16,
"justifyContent": "center",
"opacity": 1,
"transform": Array [
Object {
"rotate": "0deg",
},
],
"width": 16,
}
}
>
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [
Object {
"rotate": "73.27536734311887deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 0.7,
},
],
"width": 4,
}
}
/>
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [
Object {
"rotate": "46.49829517703514deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 0.8008696779414123,
},
],
"width": 4,
}
}
/>
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [
Object {
"rotate": "25.743213498935145deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 0.8875624559768125,
},
],
"width": 4,
}
}
/>
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [
Object {
"rotate": "11.201058030774364deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 0.9510040862404615,
},
],
"width": 4,
}
}
/>
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [
Object {
"rotate": "2.731234791722257deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 0.9881665278710133,
},
],
"width": 4,
}
}
/>
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [
Object {
"rotate": "0deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 1,
},
],
"width": 4,
}
}
/>
</View>
</View>
</View>
</View>
`;
exports[`Testing component button static button with icon only 1`] = `
<View>
<View
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Array [
Object {
"alignItems": "center",
"borderRadius": 100,
"flexDirection": "row",
"justifyContent": "center",
},
Object {
"backgroundColor": "rgb(250, 250, 250)",
"borderColor": "rgb(18, 18, 18)",
"borderWidth": 1,
"paddingHorizontal": 16,
"paddingVertical": 8,
},
undefined,
]
}
testID="base"
>
<View
style={
Array [
Object {
"opacity": 1,
},
Object {
"alignItems": "center",
"height": 16,
"justifyContent": "center",
"width": 16,
},
]
}
>
<RNSVGSvgView
align="xMidYMid"
bbHeight={16}
bbWidth={16}
className=""
color={4279374354}
focusable={false}
height={16}
meetOrSlice={0}
minX={0}
minY={0}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
style={
Array [
Object {
"backgroundColor": "transparent",
"borderWidth": 0,
},
Object {
"flex": 0,
"height": 16,
"width": 16,
},
]
}
tintColor={4279374354}
vbHeight={24}
vbWidth={24}
width={16}
>
<RNSVGGroup
propList={
Array [
"stroke",
"strokeWidth",
"strokeLinecap",
"strokeLinejoin",
]
}
stroke={
Array [
2,
]
}
strokeLinecap={1}
strokeLinejoin={1}
strokeWidth={2}
>
<RNSVGPath
d="M18 6L6 18M6 6l12 12"
/>
</RNSVGGroup>
</RNSVGSvgView>
</View>
</View>
</View>
`;
exports[`Testing component button static button with text only 1`] = `
<View>
<View
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Array [
Object {
"alignItems": "center",
"borderRadius": 100,
"flexDirection": "row",
"justifyContent": "center",
},
Object {
"backgroundColor": "rgb(250, 250, 250)",
"borderColor": "rgb(18, 18, 18)",
"borderWidth": 1,
"paddingHorizontal": 16,
"paddingVertical": 8,
},
undefined,
]
}
testID="base"
>
<Text
style={
Object {
"color": "rgb(18, 18, 18)",
"fontSize": 16,
"fontWeight": undefined,
"opacity": 1,
}
}
testID="text"
>
Test Button
</Text>
</View>
</View>
`;

View File

@ -1,6 +1,7 @@
apply plugin: "com.android.application"
import com.android.build.OutputFile
import org.apache.tools.ant.taskdefs.condition.Os
/**
* The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
@ -79,7 +80,6 @@ import com.android.build.OutputFile
project.ext.react = [
enableHermes: (findProperty('expo.jsEngine') ?: "jsc") == "hermes",
cliPath: new File(["node", "--print", "require.resolve('react-native/package.json')"].execute().text.trim(), "../cli.js")
]
apply from: new File(["node", "--print", "require.resolve('react-native/package.json')"].execute().text.trim(), "../react.gradle")
@ -121,6 +121,14 @@ def jscFlavor = 'org.webkit:android-jsc:+'
*/
def enableHermes = project.ext.react.get("enableHermes", true);
/**
* Architectures to build native code for.
*/
def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}
android {
ndkVersion rootProject.ext.ndkVersion
@ -137,17 +145,86 @@ android {
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 50
versionName "0.2"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
if (isNewArchitectureEnabled()) {
// We configure the NDK build only if you decide to opt-in for the New Architecture.
externalNativeBuild {
ndkBuild {
arguments "APP_PLATFORM=android-21",
"APP_STL=c++_shared",
"NDK_TOOLCHAIN_VERSION=clang",
"GENERATED_SRC_DIR=$buildDir/generated/source",
"PROJECT_BUILD_DIR=$buildDir",
"REACT_ANDROID_DIR=$rootDir/../node_modules/react-native/ReactAndroid",
"REACT_ANDROID_BUILD_DIR=$rootDir/../node_modules/react-native/ReactAndroid/build"
cFlags "-Wall", "-Werror", "-fexceptions", "-frtti", "-DWITH_INSPECTOR=1"
cppFlags "-std=c++17"
// Make sure this target name is the same you specify inside the
// src/main/jni/Android.mk file for the `LOCAL_MODULE` variable.
targets "tooot_appmodules"
// Fix for windows limit on number of character in file paths and in command lines
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
arguments "NDK_OUT=${rootProject.projectDir.getParent()}\\.cxx",
"NDK_APP_SHORT_COMMANDS=true"
}
}
}
if (!enableSeparateBuildPerCPUArchitecture) {
ndk {
abiFilters (*reactNativeArchitectures())
}
}
}
manifestPlaceholders = [
expoSDK: project.hasProperty('expoSDK') ? project.property('expoSDK') : "",
releaseChannel: project.hasProperty('releaseChannel') ? project.property('releaseChannel') : "default"
]
}
if (isNewArchitectureEnabled()) {
// We configure the NDK build only if you decide to opt-in for the New Architecture.
externalNativeBuild {
ndkBuild {
path "$projectDir/src/main/jni/Android.mk"
}
}
def reactAndroidProjectDir = project(':ReactAndroid').projectDir
def packageReactNdkDebugLibs = tasks.register("packageReactNdkDebugLibs", Copy) {
dependsOn(":ReactAndroid:packageReactNdkDebugLibsForBuck")
from("$reactAndroidProjectDir/src/main/jni/prebuilt/lib")
into("$buildDir/react-ndk/exported")
}
def packageReactNdkReleaseLibs = tasks.register("packageReactNdkReleaseLibs", Copy) {
dependsOn(":ReactAndroid:packageReactNdkReleaseLibsForBuck")
from("$reactAndroidProjectDir/src/main/jni/prebuilt/lib")
into("$buildDir/react-ndk/exported")
}
afterEvaluate {
// If you wish to add a custom TurboModule or component locally,
// you should uncomment this line.
// preBuild.dependsOn("generateCodegenArtifactsFromSchema")
preDebugBuild.dependsOn(packageReactNdkDebugLibs)
preReleaseBuild.dependsOn(packageReactNdkReleaseLibs)
// Due to a bug inside AGP, we have to explicitly set a dependency
// between configureNdkBuild* tasks and the preBuild tasks.
// This can be removed once this is solved: https://issuetracker.google.com/issues/207403732
configureNdkBuildRelease.dependsOn(preReleaseBuild)
configureNdkBuildDebug.dependsOn(preDebugBuild)
reactNativeArchitectures().each { architecture ->
tasks.findByName("configureNdkBuildDebug[${architecture}]")?.configure {
dependsOn("preDebugBuild")
}
tasks.findByName("configureNdkBuildRelease[${architecture}]")?.configure {
dependsOn("preReleaseBuild")
}
}
}
}
splits {
abi {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
include (*reactNativeArchitectures())
}
}
signingConfigs {
@ -241,14 +318,34 @@ dependencies {
}
}
if (isNewArchitectureEnabled()) {
// If new architecture is enabled, we let you build RN from source
// Otherwise we fallback to a prebuilt .aar bundled in the NPM package.
// This will be applied to all the imported transtitive dependency.
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute(module("com.facebook.react:react-native"))
.using(project(":ReactAndroid")).because("On New Architecture we're building React Native from source")
}
}
}
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
from configurations.implementation
into 'libs'
}
apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute().text.trim(), "../native_modules.gradle");
applyNativeModulesAppBuildGradle(project)
def isNewArchitectureEnabled() {
// To opt-in for the New Architecture, you can either:
// - Set `newArchEnabled` to true inside the `gradle.properties` file
// - Invoke gradle with `-newArchEnabled=true`
// - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
}
apply plugin: 'com.google.gms.google-services'

View File

@ -7,6 +7,6 @@
android:usesCleartextTraffic="true"
tools:targetApi="28"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false" />
</application>
</manifest>

View File

@ -19,6 +19,7 @@ import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
import com.facebook.react.ReactInstanceEventListener;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.network.NetworkingModule;
@ -48,7 +49,7 @@ public class ReactNativeFlipper {
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
if (reactContext == null) {
reactInstanceManager.addReactInstanceEventListener(
new ReactInstanceManager.ReactInstanceEventListener() {
new ReactInstanceEventListener() {
@Override
public void onReactContextInitialized(ReactContext reactContext) {
reactInstanceManager.removeReactInstanceEventListener(this);

View File

@ -19,7 +19,7 @@
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="portrait" android:documentLaunchMode="never">
<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:exported="true" android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="portrait" android:documentLaunchMode="never">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@ -31,12 +31,30 @@
<data android:scheme="tooot"/>
<data android:scheme="com.xmflsct.app.tooot"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<!-- Any other mime types you want to support -->
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*"/>
</intent-filter>
</activity>
</application>

View File

@ -13,12 +13,33 @@ public class MainActivity extends ReactActivity {
super.onCreate(null);
}
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "main";
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "main";
}
/**
* Returns the instance of the {@link ReactActivityDelegate}. There the RootView is created and
* you can specify the rendered you wish to use (Fabric or the older renderer).
*/
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegateWrapper(this, new MainActivityDelegate(this, getMainComponentName()));
}
public static class MainActivityDelegate extends ReactActivityDelegate {
public MainActivityDelegate(ReactActivity activity, String mainComponentName) {
super(activity, mainComponentName);
}
@Override
protected ReactRootView createRootView() {
ReactRootView reactRootView = new ReactRootView(getContext());
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
reactRootView.setIsFabric(BuildConfig.IS_NEW_ARCHITECTURE_ENABLED);
return reactRootView;
}
}
}

View File

@ -10,6 +10,7 @@ import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
@ -54,12 +55,19 @@ public class MainApplication extends Application implements ReactApplication {
@Override
public ReactNativeHost getReactNativeHost() {
// if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// return mNewArchitectureNativeHost;
// } else {
// return mReactNativeHost;
// }
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
// If you opted-in for the New Architecture, we enable the TurboModule system
ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
SoLoader.init(this, /* native exopackage */ false);
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());

View File

@ -1,13 +1,24 @@
import org.apache.tools.ant.taskdefs.condition.Os
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
buildToolsVersion = "30.0.2"
buildToolsVersion = "31.0.0"
minSdkVersion = 21
compileSdkVersion = 30
targetSdkVersion = 30
ndkVersion = "21.4.7075529"
kotlinVersion = '1.5.32'
compileSdkVersion = 31
targetSdkVersion = 31
if (System.properties['os.arch'] == "aarch64") {
// For M1 Users we need to use the NDK 24 which added support for aarch64
ndkVersion = "24.0.8215888"
} else if (Os.isFamily(Os.FAMILY_WINDOWS)) {
// For Android Users, we need to use NDK 23, otherwise the build will
// fail due to paths longer than the OS limit
ndkVersion = "23.1.7779620"
} else {
// Otherwise we default to the side-by-side NDK version from AGP.
ndkVersion = "21.4.7075529"
}
}
repositories {
google()
@ -16,7 +27,9 @@ buildscript {
}
dependencies {
classpath 'com.google.gms:google-services:4.3.3'
classpath("com.android.tools.build:gradle:4.2.0")
classpath("com.android.tools.build:gradle:7.0.4")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("de.undercouch:gradle-download-task:4.1.2")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

@ -26,7 +26,18 @@ android.useAndroidX=true
android.enableJetifier=true
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.75.1
FLIPPER_VERSION=0.125.0
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=false
org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=4096m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.daemon=true

Binary file not shown.

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

213
android/gradlew vendored
View File

@ -1,4 +1,4 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright 2015 the original author or authors.
@ -24,60 +24,51 @@
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@ -86,9 +77,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -97,7 +88,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
@ -105,79 +96,91 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@ -7,3 +7,9 @@ apply from: new File(["node", "--print", "require.resolve('@react-native-communi
applyNativeModulesSettingsGradle(settings)
include ':app'
includeBuild('../node_modules/react-native-gradle-plugin')
if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") {
include(":ReactAndroid")
project(":ReactAndroid").projectDir = file('../node_modules/react-native/ReactAndroid')
}

View File

@ -37,6 +37,7 @@ export default (): ExpoConfig => ({
android: {
package: 'com.xmflsct.app.tooot',
googleServicesFile: './configs/google-services.json',
permissions: ['CAMERA', 'VIBRATE']
permissions: ['CAMERA', 'VIBRATE'],
blockedPermissions: ['USE_BIOMETRIC', 'USE_FINGERPRINT']
}
})

View File

@ -3,17 +3,32 @@ require File.join(File.dirname(`node --print "require.resolve('react-native/pack
require File.join(File.dirname(`node --print "require.resolve('@react-native-community/cli-platform-ios/package.json')"`), "native_modules")
platform :ios, '12.0'
install! 'cocoapods', :deterministic_uuids => false
require 'json'
podfile_properties = JSON.parse(File.read('./Podfile.properties.json')) rescue {}
target 'tooot' do
use_expo_modules!
post_integrate do |installer|
begin
expo_patch_react_imports!(installer)
rescue => e
Pod::UI.warn e
end
end
config = use_native_modules!
# Flags change depending on the env values.
flags = get_default_flags()
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => podfile_properties['expo.jsEngine'] == 'hermes'
:hermes_enabled => podfile_properties['expo.jsEngine'] == 'hermes',
:fabric_enabled => flags[:fabric_enabled],
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/.."
)
# Enables Flipper.

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
@ -20,6 +19,7 @@
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
DA8B5B7F0DED488CAC0FF169 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = B96B72E5384D44A7B240B27E /* GoogleService-Info.plist */; };
E3BC22F5F8ABE515E14CF199 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D878F932AF7A9974E06E461 /* ExpoModulesProvider.swift */; };
E613A80B28282A01003C97D6 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = E613A80A28282A01003C97D6 /* AppDelegate.mm */; };
E633A42B281EAEAB000E540F /* ShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E633A420281EAEAB000E540F /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
E633A430281EAF38000E540F /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E633A42F281EAF38000E540F /* ShareViewController.swift */; };
/* End PBXBuildFile section */
@ -52,7 +52,6 @@
008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = "<group>"; };
13B07F961A680F5B00A75B9A /* tooot.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = tooot.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = tooot/AppDelegate.h; sourceTree = "<group>"; };
13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = tooot/AppDelegate.m; sourceTree = "<group>"; };
13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = tooot/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = tooot/Info.plist; sourceTree = "<group>"; };
@ -72,6 +71,7 @@
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = tooot/SplashScreen.storyboard; sourceTree = "<group>"; };
B96B72E5384D44A7B240B27E /* GoogleService-Info.plist */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "tooot/GoogleService-Info.plist"; sourceTree = "<group>"; };
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
E613A80A28282A01003C97D6 /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = tooot/AppDelegate.mm; sourceTree = "<group>"; };
E633A420281EAEAB000E540F /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
E633A427281EAEAB000E540F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E633A42F281EAF38000E540F /* ShareViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShareViewController.swift; path = "../../node_modules/react-native-share-menu/ios/ShareViewController.swift"; sourceTree = "<group>"; };
@ -103,10 +103,10 @@
13B07FAE1A68108700A75B9A /* tooot */ = {
isa = PBXGroup;
children = (
E613A80A28282A01003C97D6 /* AppDelegate.mm */,
BB2F792B24A3F905000567C9 /* Supporting */,
008F07F21AC5B25A0029DE68 /* main.jsbundle */,
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
13B07FB01A68108700A75B9A /* AppDelegate.m */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */,
13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
@ -465,9 +465,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */,
5EE44DD62600124E00A9BCED /* File.swift in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
E613A80B28282A01003C97D6 /* AppDelegate.mm in Sources */,
E3BC22F5F8ABE515E14CF199 /* ExpoModulesProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -539,6 +539,7 @@
"-ObjC",
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.xmflsct.app.tooot;
PRODUCT_NAME = tooot;
PROVISIONING_PROFILE_SPECIFIER = "match AdHoc com.xmflsct.app.tooot";
@ -574,6 +575,7 @@
"-ObjC",
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.xmflsct.app.tooot;
PRODUCT_NAME = tooot;
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.xmflsct.app.tooot";
@ -734,6 +736,7 @@
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.xmflsct.app.tooot.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AdHoc com.xmflsct.app.tooot.ShareExtension";
@ -777,6 +780,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.xmflsct.app.tooot.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.xmflsct.app.tooot.ShareExtension";

View File

@ -1,103 +0,0 @@
#import "AppDelegate.h"
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTLinkingManager.h>
#import <React/RCTConvert.h>
#import <RNShareMenu/ShareMenuManager.h>
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
#import <FlipperKit/FlipperClient.h>
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>
// iOS 9.x or newer
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
return [RCTLinkingManager application:application openURL:url options:options];
}
// iOS 8.x or older
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation
{
return [RCTLinkingManager application:application openURL:url
sourceApplication:sourceApplication annotation:annotation];
}
static void InitializeFlipper(UIApplication *application) {
FlipperClient *client = [FlipperClient sharedClient];
SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
[client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]];
[client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]];
[client addPlugin:[FlipperKitReactPlugin new]];
[client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]];
[client start];
}
#endif
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
InitializeFlipper(application);
#endif
RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions];
RCTRootView *rootView = [self.reactDelegate createRootViewWithBridge:bridge moduleName:@"main" initialProperties:nil];
rootView.backgroundColor = [UIColor colorNamed:@"SplashScreenBackgroundColor"];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [self.reactDelegate createRootViewController];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
[super application:application didFinishLaunchingWithOptions:launchOptions];
return YES;
}
- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge
{
// If you'd like to export some custom RCTBridgeModules that are not Expo modules, add them here!
return @[];
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
#ifdef DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
// Linking API
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
NSString *urlString = url.absoluteString;
if ([urlString hasPrefix:@"tooot-share://"]) {
NSLog(@"Entered with the following string: %@s", urlString);
return [ShareMenuManager application:application openURL:url options:options];
}
return [RCTLinkingManager application:application openURL:url options:options];
}
// Universal Links
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
@end

126
ios/tooot/AppDelegate.mm Normal file
View File

@ -0,0 +1,126 @@
#import "AppDelegate.h"
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTAppSetupUtils.h>
#import <React/RCTLinkingManager.h>
#import <RNShareMenu/ShareMenuManager.h>
#if RCT_NEW_ARCH_ENABLED
#import <React/CoreModulesPlugins.h>
#import <React/RCTCxxBridgeDelegate.h>
#import <React/RCTFabricSurfaceHostingProxyRootView.h>
#import <React/RCTSurfacePresenter.h>
#import <React/RCTSurfacePresenterBridgeAdapter.h>
#import <ReactCommon/RCTTurboModuleManager.h>
#import <react/config/ReactNativeConfig.h>
@interface AppDelegate () <RCTCxxBridgeDelegate, RCTTurboModuleManagerDelegate> {
RCTTurboModuleManager *_turboModuleManager;
RCTSurfacePresenterBridgeAdapter *_bridgeAdapter;
std::shared_ptr<const facebook::react::ReactNativeConfig> _reactNativeConfig;
facebook::react::ContextContainer::Shared _contextContainer;
}
@end
#endif
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RCTAppSetupPrepareApp(application);
RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions];
#if RCT_NEW_ARCH_ENABLED
_contextContainer = std::make_shared<facebook::react::ContextContainer const>();
_reactNativeConfig = std::make_shared<facebook::react::EmptyReactNativeConfig const>();
_contextContainer->insert("ReactNativeConfig", _reactNativeConfig);
_bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer];
bridge.surfacePresenter = _bridgeAdapter.surfacePresenter;
#endif
UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"main", nil);
if (@available(iOS 13.0, *)) {
rootView.backgroundColor = [UIColor colorNamed:@"SplashScreenBackgroundColor"];
} else {
rootView.backgroundColor = [UIColor whiteColor];
}
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [self.reactDelegate createRootViewController];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
[super application:application didFinishLaunchingWithOptions:launchOptions];
return YES;
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
#if RCT_NEW_ARCH_ENABLED
#pragma mark - RCTCxxBridgeDelegate
- (std::unique_ptr<facebook::react::JSExecutorFactory>)jsExecutorFactoryForBridge:(RCTBridge *)bridge
{
_turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge delegate:self jsInvoker:bridge.jsCallInvoker];
return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager);
}
#pragma mark RCTTurboModuleManagerDelegate
- (Class)getModuleClassFromName:(const char *)name
{
return RCTCoreModulesClassProvider(name);
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
{
return nullptr;
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name initParams: (const facebook::react::ObjCTurboModule::InitParams &)params
{
return nullptr;
}
- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
{
return RCTAppSetupDefaultModuleFromClass(moduleClass);
}
#endif
// Linking API
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
NSString *urlString = url.absoluteString;
if ([urlString hasPrefix:@"tooot-share://"]) {
NSLog(@"Entered with the following string: %@s", urlString);
return [ShareMenuManager application:application openURL:url options:options];
}
return [RCTLinkingManager application:application openURL:url options:options];
}
// Universal Links
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
@end

View File

@ -2,7 +2,7 @@
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
int main(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}

View File

@ -1,31 +0,0 @@
module.exports = {
preset: 'jest-expo',
collectCoverage: true,
collectCoverageFrom: [
'src /**/*.{ts,tsx}',
'!**/coverage /**',
'!**/node_modules /**',
'!**/app.config.ts',
'!**/babel.config.js',
'!**/jest.setup.ts'
],
setupFiles: [
'<rootDir>/jest/async-storage.js',
'<rootDir>/jest/react-native.js',
'<rootDir>/jest/react-navigation.js'
],
transformIgnorePatterns: [
'node_modules/(?!(jest-)?react-native' +
'|react-clone-referenced-element' +
'|@react-native-community' +
'|expo(nent)?' +
'|@expo(nent)?/.*' +
'|react-navigation' +
'|@react-navigation/.*|@unimodules/.*|unimodules' +
'|sentry-expo' +
'|native-base' +
'|@sentry/.*' +
'|redux-persist-expo-securestore' +
')'
]
}

View File

@ -1,3 +0,0 @@
import mockAsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock'
jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage)

View File

@ -1,6 +0,0 @@
jest.mock('react-native/Libraries/LayoutAnimation/LayoutAnimation', () => ({
...require.requireActual(
'react-native/Libraries/LayoutAnimation/LayoutAnimation'
),
configureNext: jest.fn()
}))

View File

@ -1,14 +0,0 @@
import 'react-native-gesture-handler/jestSetup'
jest.mock('react-native-reanimated', () => {
const Reanimated = require('react-native-reanimated/mock')
// The mock for `call` immediately calls the callback which is incorrect
// So we override it with a no-op
Reanimated.default.call = () => {}
return Reanimated
})
// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing
jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper')

View File

@ -1,11 +1,11 @@
{
"name": "tooot",
"versions": {
"native": "220428",
"native": "220508",
"major": 4,
"minor": 0,
"patch": 0,
"expo": "44.0.0"
"expo": "45.0.0"
},
"description": "tooot app for Mastodon",
"author": "xmflsct <me@xmflsct.com>",
@ -26,6 +26,12 @@
},
"dependencies": {
"@expo/react-native-action-sheet": "3.13.0",
"@formatjs/intl-datetimeformat": "^5.0.2",
"@formatjs/intl-getcanonicallocales": "^1.9.2",
"@formatjs/intl-locale": "^2.4.47",
"@formatjs/intl-numberformat": "^7.4.3",
"@formatjs/intl-pluralrules": "^4.3.3",
"@formatjs/intl-relativetimeformat": "^10.0.1",
"@neverdull-agency/expo-unlimited-secure-store": "1.0.10",
"@react-native-async-storage/async-storage": "1.17.3",
"@react-native-community/blur": "3.6.0",
@ -37,46 +43,48 @@
"@react-navigation/native-stack": "6.6.2",
"@react-navigation/stack": "6.2.1",
"@reduxjs/toolkit": "1.8.1",
"@sentry/react-native": "3.4.1",
"@sharcoux/slider": "6.0.2",
"@sentry/react-native": "3.4.2",
"@sharcoux/slider": "6.0.3",
"axios": "0.27.2",
"expo": "44.0.6",
"expo-auth-session": "3.5.0",
"expo-av": "10.2.1",
"expo-constants": "^13.0.2",
"expo-crypto": "10.1.2",
"expo-device": "4.1.1",
"expo-file-system": "13.2.0",
"expo-firebase-analytics": "6.0.1",
"expo-haptics": "11.1.1",
"expo-image-manipulator": "10.2.1",
"expo-image-picker": "12.0.2",
"expo-linking": "3.0.0",
"expo-localization": "12.0.1",
"expo-notifications": "0.14.1",
"expo-random": "12.1.2",
"expo-screen-capture": "4.1.1",
"expo-secure-store": "11.1.1",
"expo-splash-screen": "0.14.2",
"expo-store-review": "5.1.1",
"expo-updates": "0.11.6",
"expo-video-thumbnails": "6.2.0",
"expo-web-browser": "10.1.1",
"i18next": "21.6.16",
"expo": "45.0.1",
"expo-auth-session": "3.6.0",
"expo-av": "11.2.3",
"expo-constants": "^13.1.1",
"expo-crypto": "10.2.0",
"expo-device": "4.2.0",
"expo-file-system": "14.0.0",
"expo-firebase-analytics": "7.0.0",
"expo-haptics": "11.2.0",
"expo-image-manipulator": "10.3.1",
"expo-image-picker": "13.1.1",
"expo-linking": "3.1.0",
"expo-localization": "13.0.0",
"expo-notifications": "0.15.2",
"expo-random": "12.2.0",
"expo-screen-capture": "4.2.0",
"expo-secure-store": "11.2.0",
"expo-splash-screen": "0.15.1",
"expo-store-review": "5.2.0",
"expo-updates": "0.13.1",
"expo-video-thumbnails": "6.3.0",
"expo-web-browser": "10.2.0",
"i18next": "21.8.0",
"li": "1.3.0",
"lodash": "4.17.21",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-i18next": "11.16.7",
"react-native": "0.67.4",
"react-i18next": "11.16.9",
"react-intl": "^5.25.1",
"react-native": "0.68.1",
"react-native-animated-spinkit": "1.5.2",
"react-native-base64": "^0.2.1",
"react-native-blurhash": "1.1.10",
"react-native-fast-image": "8.5.11",
"react-native-feather": "1.1.2",
"react-native-flash-message": "0.2.1",
"react-native-gesture-handler": "2.4.1",
"react-native-gesture-handler": "2.4.2",
"react-native-htmlview": "0.16.0",
"react-native-image-keyboard": "^2.2.0",
"react-native-pager-view": "5.4.11",
"react-native-reanimated": "2.8.0",
"react-native-safe-area-context": "4.2.5",
@ -85,9 +93,8 @@
"react-native-svg": "12.3.0",
"react-native-swipe-list-view": "3.2.9",
"react-native-tab-view": "3.1.1",
"react-query": "3.38.0",
"react-query": "3.39.0",
"react-redux": "8.0.1",
"react-timeago": "6.2.1",
"redux-persist": "6.0.0",
"rn-placeholder": "3.0.3",
"sentry-expo": "4.1.1",
@ -95,7 +102,7 @@
"valid-url": "1.0.9"
},
"devDependencies": {
"@babel/core": "7.17.9",
"@babel/core": "7.17.10",
"@babel/plugin-proposal-optional-chaining": "7.16.7",
"@babel/preset-typescript": "7.16.7",
"@expo/config": "6.0.23",
@ -115,7 +122,11 @@
"patch-package": "6.4.7",
"postinstall-postinstall": "2.1.0",
"react-native-clean-project": "4.0.1",
"typescript": "4.6.3"
"typescript": "4.6.4"
},
"resolutions": {
"@types/react": "17.0.43",
"@types/react-dom": "17.0.14"
},
"expo": {
"autolinking": {

View File

@ -1,18 +1,22 @@
diff --git a/node_modules/@types/react-native-share-menu/index.d.ts b/node_modules/@types/react-native-share-menu/index.d.ts
index f52822c..b1d3bdd 100755
index f52822c..ee98565 100755
--- a/node_modules/@types/react-native-share-menu/index.d.ts
+++ b/node_modules/@types/react-native-share-menu/index.d.ts
@@ -6,9 +6,7 @@
@@ -5,11 +5,9 @@
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// Minimum TypeScript Version: 3.7
export interface ShareData {
-export interface ShareData {
- mimeType: string;
- data: string | string[];
- extraData?: object | undefined;
-}
+export type ShareData = {
+ data: {mimeType: string; data: string}[];
}
+} | {mimeType: string; data: string | string[]}
export type ShareCallback = (share?: ShareData) => void;
@@ -28,7 +26,7 @@ interface ShareMenuReactView {
dismissExtension(error?: string): void;
openApp(): void;

View File

@ -1,81 +0,0 @@
diff --git a/node_modules/expo-av/ios/EXAV/EXAV.m b/node_modules/expo-av/ios/EXAV/EXAV.m
index d255852..edf934f 100644
--- a/node_modules/expo-av/ios/EXAV/EXAV.m
+++ b/node_modules/expo-av/ios/EXAV/EXAV.m
@@ -63,7 +63,7 @@ NSString *const EXDidUpdateMetadataEventName = @"didUpdateMetadata";
@property (nonatomic, assign) BOOL audioRecorderShouldBeginRecording;
@property (nonatomic, assign) int audioRecorderDurationMillis;
-@property (nonatomic, weak) EXModuleRegistry *moduleRegistry;
+@property (nonatomic, weak) EXModuleRegistry *expoModuleRegistry;
@property (nonatomic, weak) id<EXPermissionsInterface> permissionsManager;
@end
@@ -106,7 +106,7 @@ EX_EXPORT_MODULE(ExponentAV);
- (void)installJsiBindings
{
- id<EXJavaScriptContextProvider> jsContextProvider = [_moduleRegistry getModuleImplementingProtocol:@protocol(EXJavaScriptContextProvider)];
+ id<EXJavaScriptContextProvider> jsContextProvider = [_expoModuleRegistry getModuleImplementingProtocol:@protocol(EXJavaScriptContextProvider)];
void *jsRuntimePtr = [jsContextProvider javaScriptRuntimePointer];
if (jsRuntimePtr) {
[self installJSIBindingsForRuntime:jsRuntimePtr withSoundDictionary:_soundDictionary];
@@ -131,16 +131,16 @@ EX_EXPORT_MODULE(ExponentAV);
#pragma mark - Expo experience lifecycle
-- (void)setModuleRegistry:(EXModuleRegistry *)moduleRegistry
+- (void)setModuleRegistry:(EXModuleRegistry *)expoModuleRegistry
{
- [[_moduleRegistry getModuleImplementingProtocol:@protocol(EXAppLifecycleService)] unregisterAppLifecycleListener:self];
- _moduleRegistry = moduleRegistry;
- _kernelAudioSessionManagerDelegate = [_moduleRegistry getSingletonModuleForName:@"AudioSessionManager"];
+ [[_expoModuleRegistry getModuleImplementingProtocol:@protocol(EXAppLifecycleService)] unregisterAppLifecycleListener:self];
+ _expoModuleRegistry = expoModuleRegistry;
+ _kernelAudioSessionManagerDelegate = [_expoModuleRegistry getSingletonModuleForName:@"AudioSessionManager"];
if (!_isBackgrounded) {
[_kernelAudioSessionManagerDelegate moduleDidForeground:self];
}
- [[_moduleRegistry getModuleImplementingProtocol:@protocol(EXAppLifecycleService)] registerAppLifecycleListener:self];
- _permissionsManager = [_moduleRegistry getModuleImplementingProtocol:@protocol(EXPermissionsInterface)];
+ [[_expoModuleRegistry getModuleImplementingProtocol:@protocol(EXAppLifecycleService)] registerAppLifecycleListener:self];
+ _permissionsManager = [_expoModuleRegistry getModuleImplementingProtocol:@protocol(EXPermissionsInterface)];
[EXPermissionsMethodsDelegate registerRequesters:@[[EXAudioRecordingPermissionRequester new]] withPermissionsManager:_permissionsManager];
}
@@ -478,7 +478,7 @@ withEXVideoViewForTag:(nonnull NSNumber *)reactTag
{
// TODO check that the bridge is still valid after the dispatch
// TODO check if the queues are ok
- [[_moduleRegistry getModuleImplementingProtocol:@protocol(EXUIManager)] executeUIBlock:^(id view) {
+ [[_expoModuleRegistry getModuleImplementingProtocol:@protocol(EXUIManager)] executeUIBlock:^(id view) {
if ([view isKindOfClass:[EXVideoView class]]) {
block(view);
} else {
@@ -566,7 +566,7 @@ withEXVideoViewForTag:(nonnull NSNumber *)reactTag
return EXErrorWithMessage(@"Recorder is already prepared.");
}
- id<EXFileSystemInterface> fileSystem = [_moduleRegistry getModuleImplementingProtocol:@protocol(EXFileSystemInterface)];
+ id<EXFileSystemInterface> fileSystem = [_expoModuleRegistry getModuleImplementingProtocol:@protocol(EXFileSystemInterface)];
if (!fileSystem) {
return EXErrorWithMessage(@"No FileSystem module.");
@@ -726,7 +726,7 @@ EX_EXPORT_METHOD_AS(loadForSound,
- (void)sendEventWithName:(NSString *)eventName body:(NSDictionary *)body
{
- [[_moduleRegistry getModuleImplementingProtocol:@protocol(EXEventEmitterService)] sendEventWithName:eventName body:body];
+ [[_expoModuleRegistry getModuleImplementingProtocol:@protocol(EXEventEmitterService)] sendEventWithName:eventName body:body];
}
EX_EXPORT_METHOD_AS(unloadForSound,
@@ -984,7 +984,7 @@ EX_EXPORT_METHOD_AS(unloadAudioRecorder,
- (void)dealloc
{
[_kernelAudioSessionManagerDelegate moduleWillDeallocate:self];
- [[_moduleRegistry getModuleImplementingProtocol:@protocol(EXAppLifecycleService)] unregisterAppLifecycleListener:self];
+ [[_expoModuleRegistry getModuleImplementingProtocol:@protocol(EXAppLifecycleService)] unregisterAppLifecycleListener:self];
[[NSNotificationCenter defaultCenter] removeObserver:self];
// This will clear all @properties and deactivate the audio session:

View File

@ -1,166 +0,0 @@
diff --git a/node_modules/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt b/node_modules/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt
index 53bf40f..0ba5d89 100644
--- a/node_modules/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt
+++ b/node_modules/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt
@@ -56,6 +56,7 @@ import okhttp3.Callback
import okhttp3.Headers
import okhttp3.JavaNetCookieJar
import okhttp3.MediaType
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
@@ -63,11 +64,7 @@ import okhttp3.RequestBody
import okhttp3.Response
import okhttp3.ResponseBody
-import okio.Buffer
-import okio.BufferedSource
-import okio.ForwardingSource
-import okio.Okio
-import okio.Source
+import okio.*
import org.apache.commons.codec.binary.Hex
import org.apache.commons.codec.digest.DigestUtils
@@ -766,7 +763,7 @@ open class FileSystemModule(
}
val body = createRequestBody(options, decorator, fileUri.toFile())
- return requestBuilder.method(method, body).build()
+ return method?.let { requestBuilder.method(it, body).build() }
} catch (e: Exception) {
e.message?.let { Log.e(TAG, it) }
promise.reject(e)
@@ -791,7 +788,7 @@ open class FileSystemModule(
} ?: URLConnection.guessContentTypeFromName(file.name)
val fieldName = options["fieldName"]?.let { it as String } ?: file.name
- bodyBuilder.addFormDataPart(fieldName, file.name, decorator.decorate(RequestBody.create(MediaType.parse(mimeType), file)))
+ bodyBuilder.addFormDataPart(fieldName, file.name, decorator.decorate(RequestBody.create(mimeType.toMediaTypeOrNull(), file)))
bodyBuilder.build()
}
else -> {
@@ -816,9 +813,9 @@ open class FileSystemModule(
override fun onResponse(call: Call, response: Response) {
val result = Bundle().apply {
- putString("body", response.body()?.string())
- putInt("status", response.code())
- putBundle("headers", translateHeaders(response.headers()))
+ putString("body", response.body?.string())
+ putInt("status", response.code)
+ putBundle("headers", translateHeaders(response.headers))
}
response.close()
promise.resolve(result)
@@ -866,7 +863,7 @@ open class FileSystemModule(
taskHandlers[uuid] = TaskHandler(call)
call.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
- if (call.isCanceled) {
+ if (call.isCanceled()) {
promise.resolve(null)
return
}
@@ -876,11 +873,11 @@ open class FileSystemModule(
override fun onResponse(call: Call, response: Response) {
val result = Bundle()
- val body = response.body()
+ val body = response.body
result.apply {
putString("body", body?.string())
- putInt("status", response.code())
- putBundle("headers", translateHeaders(response.headers()))
+ putInt("status", response.code)
+ putBundle("headers", translateHeaders(response.headers))
}
response.close()
promise.resolve(result)
@@ -900,10 +897,10 @@ open class FileSystemModule(
val resources = context.resources
val packageName = context.packageName
val resourceId = resources.getIdentifier(url, "raw", packageName)
- val bufferedSource = Okio.buffer(Okio.source(context.resources.openRawResource(resourceId)))
+ val bufferedSource = context.resources.openRawResource(resourceId).source().buffer()
val file = uri.toFile()
file.delete()
- val sink = Okio.buffer(Okio.sink(file))
+ val sink = file.sink().buffer()
sink.writeAll(bufferedSource)
sink.close()
val result = Bundle()
@@ -934,13 +931,13 @@ open class FileSystemModule(
override fun onResponse(call: Call, response: Response) {
val file = uri.toFile()
file.delete()
- val sink = Okio.buffer(Okio.sink(file))
- sink.writeAll(response.body()!!.source())
+ val sink = file.sink().buffer()
+ sink.writeAll(response.body!!.source())
sink.close()
val result = Bundle().apply {
putString("uri", Uri.fromFile(file).toString())
- putInt("status", response.code())
- putBundle("headers", translateHeaders(response.headers()))
+ putInt("status", response.code)
+ putBundle("headers", translateHeaders(response.headers))
if (options?.get("md5") == true) {
putString("md5", md5(file))
}
@@ -1003,7 +1000,7 @@ open class FileSystemModule(
?.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
- .body(ProgressResponseBody(originalResponse.body(), progressListener))
+ .body(ProgressResponseBody(originalResponse.body, progressListener))
.build()
}
?.build()
@@ -1098,7 +1095,7 @@ open class FileSystemModule(
val options = params[0]?.options
return try {
val response = call!!.execute()
- val responseBody = response.body()
+ val responseBody = response.body
val input = BufferedInputStream(responseBody!!.byteStream())
val output = FileOutputStream(file, isResume == true)
val data = ByteArray(1024)
@@ -1108,15 +1105,15 @@ open class FileSystemModule(
}
val result = Bundle().apply {
putString("uri", Uri.fromFile(file).toString())
- putInt("status", response.code())
- putBundle("headers", translateHeaders(response.headers()))
+ putInt("status", response.code)
+ putBundle("headers", translateHeaders(response.headers))
options?.get("md5").takeIf { it == true }?.let { putString("md5", file?.let { md5(it) }) }
}
response.close()
promise?.resolve(result)
null
} catch (e: Exception) {
- if (call?.isCanceled == true) {
+ if (call?.isCanceled() == true) {
promise?.resolve(null)
return null
}
@@ -1139,7 +1136,7 @@ open class FileSystemModule(
override fun contentLength(): Long = responseBody?.contentLength() ?: -1
override fun source(): BufferedSource =
- bufferedSource ?: Okio.buffer(source(responseBody!!.source()))
+ bufferedSource ?: source(responseBody!!.source()).buffer()
private fun source(source: Source): Source {
return object : ForwardingSource(source) {
@@ -1304,7 +1301,7 @@ open class FileSystemModule(
// Copied out of React Native's `NetworkingModule.java`
private fun translateHeaders(headers: Headers): Bundle {
val responseHeaders = Bundle()
- for (i in 0 until headers.size()) {
+ for (i in 0 until headers.size) {
val headerName = headers.name(i)
// multiple values for the same header
if (responseHeaders[headerName] != null) {

View File

@ -1,4 +1,31 @@
import { ActionSheetProvider } from '@expo/react-native-action-sheet'
import '@formatjs/intl-getcanonicallocales/polyfill'
import '@formatjs/intl-locale/polyfill'
import '@formatjs/intl-pluralrules/polyfill'
import '@formatjs/intl-pluralrules/locale-data/de'
import '@formatjs/intl-pluralrules/locale-data/en'
import '@formatjs/intl-pluralrules/locale-data/ko'
import '@formatjs/intl-pluralrules/locale-data/vi'
import '@formatjs/intl-pluralrules/locale-data/zh'
import '@formatjs/intl-numberformat/polyfill'
import '@formatjs/intl-numberformat/locale-data/de'
import '@formatjs/intl-numberformat/locale-data/en'
import '@formatjs/intl-numberformat/locale-data/ko'
import '@formatjs/intl-numberformat/locale-data/vi'
import '@formatjs/intl-numberformat/locale-data/zh'
import '@formatjs/intl-datetimeformat/polyfill'
import '@formatjs/intl-datetimeformat/locale-data/de'
import '@formatjs/intl-datetimeformat/locale-data/en'
import '@formatjs/intl-datetimeformat/locale-data/ko'
import '@formatjs/intl-datetimeformat/locale-data/vi'
import '@formatjs/intl-datetimeformat/locale-data/zh'
import '@formatjs/intl-datetimeformat/add-all-tz'
import '@formatjs/intl-relativetimeformat/polyfill'
import '@formatjs/intl-relativetimeformat/locale-data/de'
import '@formatjs/intl-relativetimeformat/locale-data/en'
import '@formatjs/intl-relativetimeformat/locale-data/ko'
import '@formatjs/intl-relativetimeformat/locale-data/vi'
import '@formatjs/intl-relativetimeformat/locale-data/zh'
import queryClient from '@helpers/queryClient'
import i18n from '@root/i18n/i18n'
import Screens from '@root/Screens'
@ -6,7 +33,9 @@ import audio from '@root/startup/audio'
import dev from '@root/startup/dev'
import log from '@root/startup/log'
import netInfo from '@root/startup/netInfo'
import push from '@root/startup/push'
import sentry from '@root/startup/sentry'
import timezone from '@root/startup/timezone'
import { persistor, store } from '@root/store'
import AccessibilityManager from '@utils/accessibility/AccessibilityManager'
import {
@ -19,11 +48,12 @@ import * as SplashScreen from 'expo-splash-screen'
import React, { useCallback, useEffect, useState } from 'react'
import { AppState, LogBox, Platform } from 'react-native'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import 'react-native-image-keyboard'
import { enableFreeze } from 'react-native-screens'
import { QueryClientProvider } from 'react-query'
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'
import push from './startup/push'
import { IntlProvider } from 'react-intl'
Platform.select({
android: LogBox.ignoreLogs(['Setting a timer for a long period of time'])
@ -33,6 +63,7 @@ dev()
sentry()
audio()
push()
timezone()
enableFreeze(true)
const App: React.FC = () => {
@ -91,13 +122,18 @@ const App: React.FC = () => {
const language = getSettingsLanguage(store.getState())
if (!language) {
store.dispatch(changeLanguage('en'))
i18n.changeLanguage('en')
} else {
i18n.changeLanguage(language)
}
i18n.changeLanguage(language)
return (
<ActionSheetProvider>
<AccessibilityManager>
<ThemeManager>
<Screens localCorrupt={localCorrupt} />
<IntlProvider locale={language}>
<Screens localCorrupt={localCorrupt} />
</IntlProvider>
</ThemeManager>
</AccessibilityManager>
</ActionSheetProvider>

View File

@ -161,61 +161,159 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
// Share Extension
const handleShare = useCallback(
(item?: { data?: { mimeType: string; data: string }[] }) => {
(
item?:
| {
data: { mimeType: string; data: string }[]
mimeType: undefined
}
| { data: string | string[]; mimeType: string }
) => {
if (instanceActive < 0) {
return
}
if (!item || !item.data || !Array.isArray(item.data) || !item.data) {
if (!item || !item.data) {
return
}
let text: string | undefined = undefined
let images: { type: string; uri: string }[] = []
let video: { type: string; uri: string } | undefined = undefined
item.data.forEach((d, i) => {
const typesImage = ['png', 'jpg', 'jpeg', 'gif']
const typesVideo = ['mp4', 'm4v', 'mov', 'webm']
const { mimeType, data } = d
console.log('mimeType', mimeType)
console.log('data', data)
if (mimeType.startsWith('image/')) {
if (!typesImage.includes(mimeType.split('/')[1])) {
console.warn('Image type not supported:', mimeType.split('/')[1])
switch (Platform.OS) {
case 'ios':
if (!Array.isArray(item.data) || !item.data) {
return
}
images.push({ type: mimeType.split('/')[1], uri: data })
} else if (mimeType.startsWith('video/')) {
if (!typesVideo.includes(mimeType.split('/')[1])) {
console.warn('Video type not supported:', mimeType.split('/')[1])
item.data.forEach(d => {
if (typeof d === 'string') return
const typesImage = ['png', 'jpg', 'jpeg', 'gif']
const typesVideo = ['mp4', 'm4v', 'mov', 'webm']
const { mimeType, data } = d
if (mimeType.startsWith('image/')) {
if (!typesImage.includes(mimeType.split('/')[1])) {
console.warn(
'Image type not supported:',
mimeType.split('/')[1]
)
displayMessage({
message: t('shareError.imageNotSupported', {
type: mimeType.split('/')[1]
}),
type: 'error',
theme
})
return
}
images.push({ type: mimeType.split('/')[1], uri: data })
} else if (mimeType.startsWith('video/')) {
if (!typesVideo.includes(mimeType.split('/')[1])) {
console.warn(
'Video type not supported:',
mimeType.split('/')[1]
)
displayMessage({
message: t('shareError.videoNotSupported', {
type: mimeType.split('/')[1]
}),
type: 'error',
theme
})
return
}
video = { type: mimeType.split('/')[1], uri: data }
} else {
if (typesImage.includes(data.split('.').pop() || '')) {
images.push({ type: data.split('.').pop()!, uri: data })
return
}
if (typesVideo.includes(data.split('.').pop() || '')) {
video = { type: data.split('.').pop()!, uri: data }
return
}
text = !text ? data : text.concat(text, `\n${data}`)
}
})
break
case 'android':
if (!item.mimeType) {
return
}
video = { type: mimeType.split('/')[1], uri: data }
} else {
if (typesImage.includes(data.split('.').pop() || '')) {
images.push({ type: data.split('.').pop()!, uri: data })
return
let tempData: string[]
if (!Array.isArray(item.data)) {
tempData = [item.data]
} else {
tempData = item.data
}
if (typesVideo.includes(data.split('.').pop() || '')) {
video = { type: data.split('.').pop()!, uri: data }
return
}
text = !text ? data : text.concat(text, `\n${data}`)
}
})
navigationRef.navigate('Screen-Compose', {
type: 'share',
text,
images,
video
})
tempData.forEach(d => {
const typesImage = ['png', 'jpg', 'jpeg', 'gif']
const typesVideo = ['mp4', 'm4v', 'mov', 'webm', 'mpeg']
if (item.mimeType!.startsWith('image/')) {
if (!typesImage.includes(item.mimeType.split('/')[1])) {
console.warn(
'Image type not supported:',
item.mimeType.split('/')[1]
)
displayMessage({
message: t('shareError.imageNotSupported', {
type: item.mimeType.split('/')[1]
}),
type: 'error',
theme
})
return
}
images.push({ type: item.mimeType.split('/')[1], uri: d })
} else if (item.mimeType.startsWith('video/')) {
if (!typesVideo.includes(item.mimeType.split('/')[1])) {
console.warn(
'Video type not supported:',
item.mimeType.split('/')[1]
)
displayMessage({
message: t('shareError.videoNotSupported', {
type: item.mimeType.split('/')[1]
}),
type: 'error',
theme
})
return
}
video = { type: item.mimeType.split('/')[1], uri: d }
} else {
if (typesImage.includes(d.split('.').pop() || '')) {
images.push({ type: d.split('.').pop()!, uri: d })
return
}
if (typesVideo.includes(d.split('.').pop() || '')) {
video = { type: d.split('.').pop()!, uri: d }
return
}
text = !text ? d : text.concat(text, `\n${d}`)
}
})
break
}
if (!text && (!images || !images.length) && !video) {
return
} else {
navigationRef.navigate('Screen-Compose', {
type: 'share',
text,
images,
video
})
}
},
[instanceActive]
)
useEffect(() => {
ShareMenu.getInitialShare(item => handleShare(item))
ShareMenu.getInitialShare(handleShare)
}, [])
useEffect(() => {
const listener = ShareMenu.addNewShareListener(item => handleShare(item))
const listener = ShareMenu.addNewShareListener(handleShare)
return () => {
listener.remove()
}

View File

@ -5,9 +5,10 @@ import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback } from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { Pressable, View } from 'react-native'
import analytics from './analytics'
import GracefullyImage from './GracefullyImage'
import CustomText from './Text'
export interface Props {
account: Mastodon.Account
@ -32,50 +33,45 @@ const ComponentAccount: React.FC<Props> = ({
return (
<Pressable
accessibilityRole='button'
style={[styles.itemDefault, styles.itemAccount]}
style={{
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
paddingVertical: StyleConstants.Spacing.M,
flexDirection: 'row',
alignItems: 'center'
}}
onPress={customOnPress || onPress}
>
<GracefullyImage
uri={{ original: account.avatar, static: account.avatar_static }}
style={styles.itemAccountAvatar}
style={{
alignSelf: 'flex-start',
width: StyleConstants.Avatar.S,
height: StyleConstants.Avatar.S,
borderRadius: 6,
marginRight: StyleConstants.Spacing.S
}}
/>
<View>
<Text numberOfLines={1}>
<CustomText numberOfLines={1}>
<ParseEmojis
content={account.display_name || account.username}
emojis={account.emojis}
size='S'
fontBold
/>
</Text>
<Text
</CustomText>
<CustomText
numberOfLines={1}
style={[styles.itemAccountAcct, { color: colors.secondary }]}
style={{
marginTop: StyleConstants.Spacing.XS,
color: colors.secondary
}}
>
@{account.acct}
</Text>
</CustomText>
</View>
</Pressable>
)
}
const styles = StyleSheet.create({
itemDefault: {
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
paddingVertical: StyleConstants.Spacing.M
},
itemAccount: {
flexDirection: 'row',
alignItems: 'center'
},
itemAccountAvatar: {
alignSelf: 'flex-start',
width: StyleConstants.Avatar.S,
height: StyleConstants.Avatar.S,
borderRadius: 6,
marginRight: StyleConstants.Spacing.S
},
itemAccountAcct: { marginTop: StyleConstants.Spacing.XS }
})
export default ComponentAccount

View File

@ -7,12 +7,11 @@ import {
AccessibilityProps,
Pressable,
StyleProp,
StyleSheet,
Text,
View,
ViewStyle
} from 'react-native'
import { Flow } from 'react-native-animated-spinkit'
import CustomText from './Text'
export interface Props {
accessibilityLabel?: AccessibilityProps['accessibilityLabel']
@ -116,7 +115,7 @@ const Button: React.FC<Props> = ({
case 'text':
return (
<>
<Text
<CustomText
style={{
color: mainColor,
fontSize:
@ -146,8 +145,10 @@ const Button: React.FC<Props> = ({
busy: loading
}}
style={[
styles.button,
{
borderRadius: 100,
justifyContent: 'center',
alignItems: 'center',
borderWidth: overlay ? 0 : 1,
borderColor: mainColor,
backgroundColor: colorBackground,
@ -170,12 +171,4 @@ const Button: React.FC<Props> = ({
)
}
const styles = StyleSheet.create({
button: {
borderRadius: 100,
justifyContent: 'center',
alignItems: 'center'
}
})
export default Button

View File

@ -1,3 +1,4 @@
import CustomText from '@components/Text'
import { useAppDispatch } from '@root/store'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { countInstanceEmoji } from '@utils/slices/instancesSlice'
@ -11,8 +12,6 @@ import {
findNodeHandle,
Pressable,
SectionList,
StyleSheet,
Text,
View
} from 'react-native'
import FastImage from 'react-native-fast-image'
@ -30,7 +29,12 @@ const EmojisList = React.memo(
const listHeader = useCallback(
({ section: { title } }) => (
<Text style={[styles.group, { color: colors.secondary }]}>{title}</Text>
<CustomText
fontStyle='S'
style={{ position: 'absolute', color: colors.secondary }}
>
{title}
</CustomText>
),
[]
)
@ -38,7 +42,15 @@ const EmojisList = React.memo(
const listItem = useCallback(
({ index, item }: { item: Mastodon.Emoji[]; index: number }) => {
return (
<View key={index} style={styles.emojis}>
<View
key={index}
style={{
flex: 1,
flexWrap: 'wrap',
marginTop: StyleConstants.Spacing.M,
marginRight: StyleConstants.Spacing.S
}}
>
{item.map(emoji => {
const uri = reduceMotionEnabled ? emoji.static_url : emoji.url
if (validUrl.isHttpsUri(uri)) {
@ -64,7 +76,12 @@ const EmojisList = React.memo(
'screenCompose:content.root.footer.emojis.accessibilityHint'
)}
source={{ uri }}
style={styles.emoji}
style={{
width: 32,
height: 32,
padding: StyleConstants.Spacing.S,
margin: StyleConstants.Spacing.S
}}
/>
</Pressable>
)
@ -104,23 +121,4 @@ const EmojisList = React.memo(
() => true
)
const styles = StyleSheet.create({
group: {
position: 'absolute',
...StyleConstants.FontStyle.S
},
emojis: {
flex: 1,
flexWrap: 'wrap',
marginTop: StyleConstants.Spacing.M,
marginRight: StyleConstants.Spacing.S
},
emoji: {
width: 32,
height: 32,
padding: StyleConstants.Spacing.S,
margin: StyleConstants.Spacing.S
}
})
export default EmojisList

View File

@ -4,8 +4,9 @@ import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback } from 'react'
import { Pressable, StyleSheet, Text } from 'react-native'
import { Pressable } from 'react-native'
import analytics from './analytics'
import CustomText from './Text'
export interface Props {
hashtag: Mastodon.Tag
@ -30,23 +31,14 @@ const ComponentHashtag: React.FC<Props> = ({
return (
<Pressable
accessibilityRole='button'
style={styles.itemDefault}
style={{ padding: StyleConstants.Spacing.S * 1.5 }}
onPress={customOnPress || onPress}
>
<Text style={[styles.itemHashtag, { color: colors.primaryDefault }]}>
<CustomText fontStyle='M' style={{ color: colors.primaryDefault }}>
#{hashtag.name}
</Text>
</CustomText>
</Pressable>
)
}
const styles = StyleSheet.create({
itemDefault: {
padding: StyleConstants.Spacing.S * 1.5
},
itemHashtag: {
...StyleConstants.FontStyle.M
}
})
export default ComponentHashtag

View File

@ -1,7 +1,7 @@
import CustomText from '@components/Text'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { StyleSheet, Text } from 'react-native'
export interface Props {
content: string
@ -14,11 +14,12 @@ const HeaderCenter = React.memo(
const { colors } = useTheme()
return (
<Text
style={[
styles.text,
{ color: inverted ? colors.primaryOverlay : colors.primaryDefault }
]}
<CustomText
style={{
fontSize: 18,
fontWeight: StyleConstants.Font.Weight.Bold,
color: inverted ? colors.primaryOverlay : colors.primaryDefault
}}
children={content}
/>
)
@ -26,11 +27,4 @@ const HeaderCenter = React.memo(
(prev, next) => prev.content === next.content
)
const styles = StyleSheet.create({
text: {
fontSize: 18,
fontWeight: StyleConstants.Font.Weight.Bold
}
})
export default HeaderCenter

View File

@ -1,8 +1,9 @@
import Icon from '@components/Icon'
import CustomText from '@components/Text'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react'
import { Pressable, StyleSheet, Text } from 'react-native'
import { Pressable } from 'react-native'
export interface Props {
type?: 'icon' | 'text'
@ -34,8 +35,9 @@ const HeaderLeft: React.FC<Props> = ({
)
case 'text':
return (
<Text
style={[styles.text, { color: colors.primaryDefault }]}
<CustomText
fontStyle='M'
style={{ color: colors.primaryDefault }}
children={content}
/>
)
@ -46,38 +48,27 @@ const HeaderLeft: React.FC<Props> = ({
<Pressable
onPress={onPress}
children={children}
style={[
styles.base,
{
backgroundColor: background
? colors.backgroundOverlayDefault
: undefined,
minHeight: 44,
minWidth: 44,
marginLeft: native
? -StyleConstants.Spacing.S
: StyleConstants.Spacing.S,
...(type === 'icon' && {
borderRadius: 100
}),
...(type === 'text' && {
paddingHorizontal: StyleConstants.Spacing.S
})
}
]}
style={{
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: background
? colors.backgroundOverlayDefault
: undefined,
minHeight: 44,
minWidth: 44,
marginLeft: native
? -StyleConstants.Spacing.S
: StyleConstants.Spacing.S,
...(type === 'icon' && {
borderRadius: 100
}),
...(type === 'text' && {
paddingHorizontal: StyleConstants.Spacing.S
})
}}
/>
)
}
const styles = StyleSheet.create({
base: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center'
},
text: {
...StyleConstants.FontStyle.M
}
})
export default HeaderLeft

View File

@ -1,14 +1,9 @@
import Icon from '@components/Icon'
import CustomText from '@components/Text'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react'
import {
AccessibilityProps,
Pressable,
StyleSheet,
Text,
View
} from 'react-native'
import { AccessibilityProps, Pressable, View } from 'react-native'
import { Flow } from 'react-native-animated-spinkit'
export interface Props {
@ -72,14 +67,12 @@ const HeaderRight: React.FC<Props> = ({
case 'text':
return (
<>
<Text
style={[
styles.text,
{
color: disabled ? colors.secondary : colors.primaryDefault,
opacity: loading ? 0 : 1
}
]}
<CustomText
fontStyle='M'
style={{
color: disabled ? colors.secondary : colors.primaryDefault,
opacity: loading ? 0 : 1
}}
children={content}
/>
{loading && loadingSpinkit}
@ -97,38 +90,27 @@ const HeaderRight: React.FC<Props> = ({
onPress={onPress}
children={children}
disabled={disabled || loading}
style={[
styles.base,
{
backgroundColor: background
? colors.backgroundOverlayDefault
: undefined,
minHeight: 44,
minWidth: 44,
marginRight: native
? -StyleConstants.Spacing.S
: StyleConstants.Spacing.S,
...(type === 'icon' && {
borderRadius: 100
}),
...(type === 'text' && {
paddingHorizontal: StyleConstants.Spacing.S
})
}
]}
style={{
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: background
? colors.backgroundOverlayDefault
: undefined,
minHeight: 44,
minWidth: 44,
marginRight: native
? -StyleConstants.Spacing.S
: StyleConstants.Spacing.S,
...(type === 'icon' && {
borderRadius: 100
}),
...(type === 'text' && {
paddingHorizontal: StyleConstants.Spacing.S
})
}}
/>
)
}
const styles = StyleSheet.create({
base: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center'
},
text: {
...StyleConstants.FontStyle.M
}
})
export default HeaderRight

View File

@ -9,17 +9,11 @@ import React, {
useRef,
useState
} from 'react'
import {
Platform,
StyleSheet,
Text,
TextInput,
TextInputProps,
View
} from 'react-native'
import { Platform, TextInput, TextInputProps, View } from 'react-native'
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
import { ComponentEmojis, EmojisButton, EmojisList } from './Emojis'
import EmojisContext from './Emojis/helpers/EmojisContext'
import CustomText from './Text'
export interface Props {
autoFocus?: boolean
@ -106,14 +100,14 @@ const Input: React.FC<Props> = ({
maxLength={options?.maxLength}
>
<View
style={[
styles.base,
{
borderColor: colors.border,
flexDirection: multiline ? 'column' : 'row',
alignItems: 'stretch'
}
]}
style={{
borderWidth: 1,
marginVertical: StyleConstants.Spacing.S,
padding: StyleConstants.Spacing.S,
borderColor: colors.border,
flexDirection: multiline ? 'column' : 'row',
alignItems: 'stretch'
}}
>
<EmojisContext.Consumer>
{({ emojisDispatch }) => (
@ -124,16 +118,15 @@ const Input: React.FC<Props> = ({
setInputFocused(false)
emojisDispatch({ type: 'activate', payload: false })
}}
style={[
styles.textInput,
{
color: colors.primaryDefault,
minHeight:
Platform.OS === 'ios' && multiline
? StyleConstants.Font.LineHeight.M * 5
: undefined
}
]}
style={{
flex: 1,
fontSize: StyleConstants.Font.Size.M,
color: colors.primaryDefault,
minHeight:
Platform.OS === 'ios' && multiline
? StyleConstants.Font.LineHeight.M * 5
: undefined
}}
onChangeText={setValue}
onSelectionChange={onSelectionChange}
value={value}
@ -149,16 +142,25 @@ const Input: React.FC<Props> = ({
</EmojisContext.Consumer>
{title ? (
<Animated.Text
style={[styles.title, animateTitle, { color: colors.secondary }]}
style={[
animateTitle,
{ position: 'absolute', color: colors.secondary }
]}
>
{title}
</Animated.Text>
) : null}
<View style={{ flexDirection: 'row', alignSelf: 'flex-end' }}>
{options?.maxLength && value?.length ? (
<Text style={[styles.maxLength, { color: colors.secondary }]}>
<CustomText
fontStyle='S'
style={{
paddingLeft: StyleConstants.Spacing.XS,
color: colors.secondary
}}
>
{value?.length} / {options.maxLength}
</Text>
</CustomText>
) : null}
{inputFocused ? <EmojisButton /> : null}
</View>
@ -168,24 +170,4 @@ const Input: React.FC<Props> = ({
)
}
const styles = StyleSheet.create({
base: {
alignItems: 'flex-end',
borderWidth: 1,
marginVertical: StyleConstants.Spacing.S,
padding: StyleConstants.Spacing.S
},
title: {
position: 'absolute'
},
textInput: {
flex: 1,
fontSize: StyleConstants.Font.Size.M
},
maxLength: {
...StyleConstants.FontStyle.S,
paddingLeft: StyleConstants.Spacing.XS
}
})
export default Input

View File

@ -15,8 +15,6 @@ import {
Image,
KeyboardAvoidingView,
Platform,
StyleSheet,
Text,
TextInput,
View
} from 'react-native'
@ -26,6 +24,7 @@ import { Placeholder } from 'rn-placeholder'
import analytics from './analytics'
import InstanceAuth from './Instance/Auth'
import InstanceInfo from './Instance/Info'
import CustomText from './Text'
export interface Props {
scrollViewRef?: RefObject<ScrollView>
@ -134,40 +133,50 @@ const ComponentInstance: React.FC<Props> = ({
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
{!disableHeaderImage ? (
<View style={styles.imageContainer}>
<View style={{ flexDirection: 'row' }}>
<Image
source={require('assets/images/welcome.png')}
style={styles.image}
style={{ resizeMode: 'contain', flex: 1, aspectRatio: 16 / 9 }}
/>
</View>
) : null}
<View style={styles.base}>
<View style={styles.inputRow}>
<View
style={{
marginTop: StyleConstants.Spacing.L,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
}}
>
<View
style={{
flexDirection: 'row',
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
}}
>
<TextInput
accessible={false}
accessibilityRole='none'
style={[
styles.prefix,
{
color: colors.primaryDefault,
borderBottomColor: instanceQuery.isError
? colors.red
: colors.border
}
]}
style={{
borderBottomWidth: 1,
...StyleConstants.FontStyle.M,
color: colors.primaryDefault,
borderBottomColor: instanceQuery.isError
? colors.red
: colors.border
}}
editable={false}
defaultValue='https://'
/>
<TextInput
style={[
styles.textInput,
{
color: colors.primaryDefault,
borderBottomColor: instanceQuery.isError
? colors.red
: colors.border
}
]}
style={{
flex: 1,
borderBottomWidth: 1,
...StyleConstants.FontStyle.M,
marginRight: StyleConstants.Spacing.M,
color: colors.primaryDefault,
borderBottomColor: instanceQuery.isError
? colors.red
: colors.border
}}
onChangeText={onChangeText}
autoCapitalize='none'
clearButtonMode='never'
@ -205,9 +214,9 @@ const ComponentInstance: React.FC<Props> = ({
content={instanceQuery.data?.title || undefined}
potentialWidth={2}
/>
<View style={styles.instanceStats}>
<View style={{ flex: 1, flexDirection: 'row' }}>
<InstanceInfo
style={styles.stat1}
style={{ alignItems: 'flex-start' }}
header={t('server.information.accounts')}
content={
instanceQuery.data?.stats?.user_count?.toString() || undefined
@ -215,7 +224,7 @@ const ComponentInstance: React.FC<Props> = ({
potentialWidth={4}
/>
<InstanceInfo
style={styles.stat2}
style={{ alignItems: 'center' }}
header={t('server.information.statuses')}
content={
instanceQuery.data?.stats?.status_count?.toString() ||
@ -224,7 +233,7 @@ const ComponentInstance: React.FC<Props> = ({
potentialWidth={4}
/>
<InstanceInfo
style={styles.stat3}
style={{ alignItems: 'flex-end' }}
header={t('server.information.domains')}
content={
instanceQuery.data?.stats?.domain_count?.toString() ||
@ -234,15 +243,28 @@ const ComponentInstance: React.FC<Props> = ({
/>
</View>
</Placeholder>
<View style={styles.disclaimer}>
<View
style={{
flexDirection: 'row',
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
marginVertical: StyleConstants.Spacing.M
}}
>
<Icon
name='Lock'
size={StyleConstants.Font.Size.S}
color={colors.secondary}
style={styles.disclaimerIcon}
style={{
marginTop:
(StyleConstants.Font.LineHeight.S -
StyleConstants.Font.Size.S) /
2,
marginRight: StyleConstants.Spacing.XS
}}
/>
<Text
style={[styles.disclaimerText, { color: colors.secondary }]}
<CustomText
fontStyle='S'
style={{ flex: 1, color: colors.secondary }}
accessibilityRole='link'
onPress={() => {
if (screenReaderEnabled) {
@ -254,7 +276,7 @@ const ComponentInstance: React.FC<Props> = ({
}}
>
{t('server.disclaimer.base')}
<Text
<CustomText
accessible
style={{ color: colors.blue }}
onPress={() => {
@ -265,8 +287,8 @@ const ComponentInstance: React.FC<Props> = ({
}}
>
{t('server.disclaimer.privacy')}
</Text>
</Text>
</CustomText>
</CustomText>
</View>
</View>
</View>
@ -276,54 +298,4 @@ const ComponentInstance: React.FC<Props> = ({
)
}
const styles = StyleSheet.create({
imageContainer: { flexDirection: 'row' },
image: { resizeMode: 'contain', flex: 1, aspectRatio: 16 / 9 },
base: {
marginTop: StyleConstants.Spacing.L,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
},
inputRow: {
flexDirection: 'row',
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
},
prefix: {
borderBottomWidth: 1,
...StyleConstants.FontStyle.M
},
textInput: {
flex: 1,
borderBottomWidth: 1,
...StyleConstants.FontStyle.M,
marginRight: StyleConstants.Spacing.M
},
instanceStats: {
flex: 1,
flexDirection: 'row'
},
stat1: {
alignItems: 'flex-start'
},
stat2: {
alignItems: 'center'
},
stat3: {
alignItems: 'flex-end'
},
disclaimer: {
flexDirection: 'row',
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
marginVertical: StyleConstants.Spacing.M
},
disclaimerIcon: {
marginTop:
(StyleConstants.Font.LineHeight.S - StyleConstants.Font.Size.S) / 2,
marginRight: StyleConstants.Spacing.XS
},
disclaimerText: {
flex: 1,
...StyleConstants.FontStyle.S
}
})
export default ComponentInstance

View File

@ -1,7 +1,8 @@
import CustomText from '@components/Text'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { StyleSheet, Text, View, ViewStyle } from 'react-native'
import { View, ViewStyle } from 'react-native'
import { PlaceholderLine } from 'rn-placeholder'
export interface Props {
@ -16,14 +17,33 @@ const InstanceInfo = React.memo(
const { colors } = useTheme()
return (
<View style={[styles.base, style]} accessible>
<Text style={[styles.header, { color: colors.primaryDefault }]}>
{header}
</Text>
<View
style={[
{
flex: 1,
marginTop: StyleConstants.Spacing.M,
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
paddingRight: StyleConstants.Spacing.Global.PagePadding
},
style
]}
accessible
>
<CustomText
fontStyle='S'
style={{
fontWeight: StyleConstants.Font.Weight.Bold,
marginBottom: StyleConstants.Spacing.XS,
color: colors.primaryDefault
}}
children={header}
/>
{content ? (
<Text style={[styles.content, { color: colors.primaryDefault }]}>
{content}
</Text>
<CustomText
fontStyle='M'
style={{ color: colors.primaryDefault }}
children={content}
/>
) : (
<PlaceholderLine
width={
@ -43,21 +63,4 @@ const InstanceInfo = React.memo(
(prev, next) => prev.content === next.content
)
const styles = StyleSheet.create({
base: {
flex: 1,
marginTop: StyleConstants.Spacing.M,
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
paddingRight: StyleConstants.Spacing.Global.PagePadding
},
header: {
...StyleConstants.FontStyle.S,
fontWeight: StyleConstants.Font.Weight.Bold,
marginBottom: StyleConstants.Spacing.XS
},
content: {
...StyleConstants.FontStyle.M
}
})
export default InstanceInfo

View File

@ -1,7 +1,8 @@
import React from 'react'
import { StyleSheet, Text, View } from 'react-native'
import { View } from 'react-native'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import CustomText from '@components/Text'
export interface Props {
heading: string
@ -11,20 +12,16 @@ const MenuHeader: React.FC<Props> = ({ heading }) => {
const { colors } = useTheme()
return (
<View style={styles.base}>
<Text style={[styles.text, { color: colors.secondary }]}>{heading}</Text>
<View style={{ paddingBottom: StyleConstants.Spacing.S }}>
<CustomText
fontStyle='S'
fontWeight='Bold'
style={{ color: colors.secondary }}
>
{heading}
</CustomText>
</View>
)
}
const styles = StyleSheet.create({
base: {
paddingBottom: StyleConstants.Spacing.S
},
text: {
...StyleConstants.FontStyle.S,
fontWeight: StyleConstants.Font.Weight.Bold
}
})
export default MenuHeader

View File

@ -1,4 +1,5 @@
import Icon from '@components/Icon'
import CustomText from '@components/Text'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
@ -99,12 +100,13 @@ const MenuRow: React.FC<Props> = ({
/>
) : null}
<View style={styles.main}>
<Text
style={[styles.title, { color: colors.primaryDefault }]}
<CustomText
fontStyle='M'
style={{ color: colors.primaryDefault }}
numberOfLines={1}
>
{title}
</Text>
</CustomText>
</View>
</View>
@ -112,18 +114,15 @@ const MenuRow: React.FC<Props> = ({
<View style={styles.back}>
{content ? (
typeof content === 'string' ? (
<Text
style={[
styles.content,
{
color: colors.secondary,
opacity: !iconBack && loading ? 0 : 1
}
]}
<CustomText
style={{
color: colors.secondary,
opacity: !iconBack && loading ? 0 : 1
}}
numberOfLines={1}
>
{content}
</Text>
</CustomText>
) : (
content
)
@ -150,9 +149,9 @@ const MenuRow: React.FC<Props> = ({
) : null}
</View>
{description ? (
<Text style={[styles.description, { color: colors.secondary }]}>
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
{description}
</Text>
</CustomText>
) : null}
</View>
</TapGestureHandler>
@ -187,9 +186,6 @@ const styles = StyleSheet.create({
main: {
flex: 1
},
title: {
...StyleConstants.FontStyle.M
},
description: {
...StyleConstants.FontStyle.S
},

View File

@ -59,7 +59,7 @@ const displayMessage = ({
if (ref) {
ref.current?.showMessage({
duration: type === 'error' ? 5000 : duration === 'short' ? 1500 : 3000,
duration: type === 'error' ? 8000 : duration === 'short' ? 3000 : 5000,
autoHide,
message,
description,

View File

@ -1,10 +1,11 @@
import CustomText from '@components/Text'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { getSettingsFontsize } from '@utils/slices/settingsSlice'
import { StyleConstants } from '@utils/styles/constants'
import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react'
import { StyleSheet, Text } from 'react-native'
import { StyleSheet } from 'react-native'
import FastImage from 'react-native-fast-image'
import { useSelector } from 'react-redux'
import validUrl from 'valid-url'
@ -57,7 +58,7 @@ const ParseEmojis = React.memo(
}, [theme, adaptiveFontsize])
return (
<Text style={styles.text}>
<CustomText style={styles.text}>
{emojis ? (
content
.split(regexEmoji)
@ -69,30 +70,34 @@ const ParseEmojis = React.memo(
return emojiShortcode === `:${emoji.shortcode}:`
})
if (emojiIndex === -1) {
return <Text key={emojiShortcode + i}>{emojiShortcode}</Text>
return (
<CustomText key={emojiShortcode + i}>
{emojiShortcode}
</CustomText>
)
} else {
const uri = reduceMotionEnabled
? emojis[emojiIndex].static_url
: emojis[emojiIndex].url
if (validUrl.isHttpsUri(uri)) {
return (
<Text key={emojiShortcode + i}>
<CustomText key={emojiShortcode + i}>
{i === 0 ? ' ' : undefined}
<FastImage source={{ uri }} style={styles.image} />
</Text>
</CustomText>
)
} else {
return null
}
}
} else {
return <Text key={i}>{str}</Text>
return <CustomText key={i}>{str}</CustomText>
}
})
) : (
<Text>{content}</Text>
<CustomText>{content}</CustomText>
)}
</Text>
</CustomText>
)
},
(prev, next) => prev.content === next.content

View File

@ -2,6 +2,7 @@ import analytics from '@components/analytics'
import Icon from '@components/Icon'
import openLink from '@components/openLink'
import ParseEmojis from '@components/Parse/Emojis'
import CustomText from '@components/Text'
import { useNavigation, useRoute } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
@ -12,7 +13,7 @@ import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, Text, View } from 'react-native'
import { Pressable, View } from 'react-native'
import HTMLView from 'react-native-htmlview'
import { useSelector } from 'react-redux'
@ -53,7 +54,7 @@ const renderNode = ({
? routeParams.hashtag !== tag[1] && routeParams.hashtag !== tag[2]
: true
return (
<Text
<CustomText
accessible
key={index}
style={{
@ -72,7 +73,7 @@ const renderNode = ({
>
{node.children[0].data}
{node.children[1]?.children[0].data}
</Text>
</CustomText>
)
} else if (classes.includes('mention') && mentions) {
const accountIndex = mentions.findIndex(
@ -82,7 +83,7 @@ const renderNode = ({
? routeParams.account.id !== mentions[accountIndex]?.id
: true
return (
<Text
<CustomText
key={index}
style={{
color:
@ -102,7 +103,7 @@ const renderNode = ({
>
{node.children[0].data}
{node.children[1]?.children[0].data}
</Text>
</CustomText>
)
}
} else {
@ -113,7 +114,7 @@ const renderNode = ({
const shouldBeTag =
tags && tags.filter(tag => `#${tag.name}` === content).length > 0
return (
<Text
<CustomText
key={index}
style={{
color: colors.blue,
@ -142,7 +143,7 @@ const renderNode = ({
}}
/>
) : null}
</Text>
</CustomText>
)
}
break
@ -252,7 +253,7 @@ const ParseHTML = React.memo(
return (
<View style={{ overflow: 'hidden' }}>
<Text
<CustomText
children={children}
onTextLayout={onTextLayout}
numberOfLines={
@ -275,7 +276,7 @@ const ParseHTML = React.memo(
backgroundColor: colors.backgroundDefault
}}
>
<Text
<CustomText
style={{
textAlign: 'center',
...StyleConstants.FontStyle.S,

View File

@ -1,24 +0,0 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Text } from 'react-native'
import TimeAgo from 'react-timeago'
// @ts-ignore
import buildFormatter from 'react-timeago/lib/formatters/buildFormatter'
export interface Props {
date: string | number
}
const RelativeTime: React.FC<Props> = ({ date }) => {
const { t } = useTranslation('componentRelativeTime')
return (
<TimeAgo
date={date}
component={Text}
formatter={buildFormatter(t('strings', { returnObjects: true }))}
/>
)
}
export default RelativeTime

60
src/components/Text.tsx Normal file
View File

@ -0,0 +1,60 @@
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { StyleConstants } from '@utils/styles/constants'
import { Text, TextProps, TextStyle } from 'react-native'
type Props =
| {
style?: Omit<TextStyle, 'fontSize' | 'lineHeight' | 'fontWeight'>
fontStyle?: undefined
fontSize?: 'S' | 'M' | 'L'
lineHeight?: 'S' | 'M' | 'L'
fontWeight?: 'Normal' | 'Bold'
}
| {
style?: Omit<TextStyle, 'fontSize' | 'lineHeight' | 'fontWeight'>
fontStyle: 'S' | 'M' | 'L'
fontSize?: undefined
lineHeight?: undefined
fontWeight?: 'Normal' | 'Bold'
}
const CustomText: React.FC<Props & TextProps> = ({
children,
style,
fontStyle,
fontSize,
fontWeight = 'Normal',
lineHeight,
...rest
}) => {
const { boldTextEnabled } = useAccessibility()
enum BoldMapping {
'Normal' = '600',
'Bold' = '800'
}
return (
<Text
style={[
style,
{ ...(fontStyle && StyleConstants.FontStyle[fontStyle]) },
{ ...(fontSize && { fontSize: StyleConstants.Font.Size[fontSize] }) },
{
...(lineHeight && {
lineHeight: StyleConstants.Font.LineHeight[lineHeight]
})
},
{
fontWeight: boldTextEnabled
? BoldMapping[fontWeight]
: StyleConstants.Font.Weight[fontWeight]
}
]}
{...rest}
children={children}
/>
)
}
export default CustomText

View File

@ -102,6 +102,7 @@ const TimelineDefault = React.memo(
queryKey={disableOnPress ? undefined : queryKey}
rootQueryKey={disableOnPress ? undefined : rootQueryKey}
status={actualStatus}
highlighted={highlighted}
/>
</View>

View File

@ -1,12 +1,13 @@
import analytics from '@components/analytics'
import Button from '@components/Button'
import Icon from '@components/Icon'
import CustomText from '@components/Text'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native'
import { View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
export interface Props {
@ -40,9 +41,16 @@ const TimelineEmpty = React.memo(
size={StyleConstants.Font.Size.L}
color={colors.primaryDefault}
/>
<Text style={[styles.error, { color: colors.primaryDefault }]}>
<CustomText
fontStyle='M'
style={{
marginTop: StyleConstants.Spacing.S,
marginBottom: StyleConstants.Spacing.L,
color: colors.primaryDefault
}}
>
{t('empty.error.message')}
</Text>
</CustomText>
<Button
type='text'
content={t('empty.error.button')}
@ -61,9 +69,16 @@ const TimelineEmpty = React.memo(
size={StyleConstants.Font.Size.L}
color={colors.primaryDefault}
/>
<Text style={[styles.error, { color: colors.primaryDefault }]}>
<CustomText
fontStyle='M'
style={{
marginTop: StyleConstants.Spacing.S,
marginBottom: StyleConstants.Spacing.L,
color: colors.primaryDefault
}}
>
{t('empty.success.message')}
</Text>
</CustomText>
</>
)
}
@ -85,12 +100,4 @@ const TimelineEmpty = React.memo(
() => true
)
const styles = StyleSheet.create({
error: {
...StyleConstants.FontStyle.M,
marginTop: StyleConstants.Spacing.S,
marginBottom: StyleConstants.Spacing.L
}
})
export default TimelineEmpty

View File

@ -1,10 +1,11 @@
import Icon from '@components/Icon'
import CustomText from '@components/Text'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { Trans } from 'react-i18next'
import { Text, View } from 'react-native'
import { View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
export interface Props {
@ -38,9 +39,7 @@ const TimelineFooter = React.memo(
{!disableInfinity && hasNextPage ? (
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
) : (
<Text
style={{ ...StyleConstants.FontStyle.S, color: colors.secondary }}
>
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
<Trans
i18nKey='componentTimeline:end.message'
components={[
@ -51,7 +50,7 @@ const TimelineFooter = React.memo(
/>
]}
/>
</Text>
</CustomText>
)}
</View>
)

View File

@ -1,8 +1,9 @@
import CustomText from '@components/Text'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Text, View } from 'react-native'
import { View } from 'react-native'
const TimelineLookback = React.memo(
() => {
@ -19,14 +20,9 @@ const TimelineLookback = React.memo(
backgroundColor: colors.backgroundDefault
}}
>
<Text
style={{
...StyleConstants.FontStyle.S,
color: colors.primaryDefault
}}
>
<CustomText fontStyle='S' style={{ color: colors.primaryDefault }}>
{t('lookback.message')}
</Text>
</CustomText>
</View>
)
},

View File

@ -1,5 +1,6 @@
import haptics from '@components/haptics'
import Icon from '@components/Icon'
import CustomText from '@components/Text'
import {
QueryKeyTimeline,
TimelineData,
@ -9,7 +10,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList, Platform, StyleSheet, Text, View } from 'react-native'
import { FlatList, Platform, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import Animated, {
Extrapolate,
@ -251,9 +252,18 @@ const TimelineRefresh: React.FC<Props> = ({
return (
<Animated.View style={headerPadding}>
<View style={styles.base}>
<View
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: CONTAINER_HEIGHT * 2,
alignItems: 'center'
}}
>
{isFetching ? (
<View style={styles.container2}>
<View style={{ height: CONTAINER_HEIGHT, justifyContent: 'center' }}>
<Circle
size={StyleConstants.Font.Size.L}
color={colors.secondary}
@ -261,9 +271,19 @@ const TimelineRefresh: React.FC<Props> = ({
</View>
) : (
<>
<View style={styles.container1}>
<Text
style={[styles.explanation, { color: colors.primaryDefault }]}
<View
style={{
flex: 1,
flexDirection: 'row',
height: CONTAINER_HEIGHT
}}
>
<CustomText
fontStyle='S'
style={{
lineHeight: CONTAINER_HEIGHT,
color: colors.primaryDefault
}}
onLayout={onLayout}
children={t('refresh.fetchPreviousPage')}
/>
@ -285,9 +305,15 @@ const TimelineRefresh: React.FC<Props> = ({
}
/>
</View>
<View style={styles.container2}>
<Text
style={[styles.explanation, { color: colors.primaryDefault }]}
<View
style={{ height: CONTAINER_HEIGHT, justifyContent: 'center' }}
>
<CustomText
fontStyle='S'
style={{
lineHeight: CONTAINER_HEIGHT,
color: colors.primaryDefault
}}
onLayout={onLayout}
children={t('refresh.refetch')}
/>
@ -299,25 +325,4 @@ const TimelineRefresh: React.FC<Props> = ({
)
}
const styles = StyleSheet.create({
base: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: CONTAINER_HEIGHT * 2,
alignItems: 'center'
},
container1: {
flex: 1,
flexDirection: 'row',
height: CONTAINER_HEIGHT
},
container2: { height: CONTAINER_HEIGHT, justifyContent: 'center' },
explanation: {
fontSize: StyleConstants.Font.Size.S,
lineHeight: CONTAINER_HEIGHT
}
})
export default TimelineRefresh

View File

@ -1,6 +1,7 @@
import analytics from '@components/analytics'
import Icon from '@components/Icon'
import { displayMessage } from '@components/Message'
import CustomText from '@components/Text'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { RootStackParamList } from '@utils/navigation/navigators'
@ -13,7 +14,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { Pressable, StyleSheet, View } from 'react-native'
import { useQueryClient } from 'react-query'
export interface Props {
@ -185,7 +186,7 @@ const TimelineActions: React.FC<Props> = ({
size={StyleConstants.Font.Size.L}
/>
{status.replies_count > 0 ? (
<Text
<CustomText
style={{
color: colors.secondary,
fontSize: StyleConstants.Font.Size.M,
@ -193,7 +194,7 @@ const TimelineActions: React.FC<Props> = ({
}}
>
{status.replies_count}
</Text>
</CustomText>
) : null}
</>
),
@ -213,7 +214,7 @@ const TimelineActions: React.FC<Props> = ({
size={StyleConstants.Font.Size.L}
/>
{status.reblogs_count > 0 ? (
<Text
<CustomText
style={{
color: color(status.reblogged),
fontSize: StyleConstants.Font.Size.M,
@ -221,7 +222,7 @@ const TimelineActions: React.FC<Props> = ({
}}
>
{status.reblogs_count}
</Text>
</CustomText>
) : null}
</>
)
@ -236,7 +237,7 @@ const TimelineActions: React.FC<Props> = ({
size={StyleConstants.Font.Size.L}
/>
{status.favourites_count > 0 ? (
<Text
<CustomText
style={{
color: color(status.favourited),
fontSize: StyleConstants.Font.Size.M,
@ -245,7 +246,7 @@ const TimelineActions: React.FC<Props> = ({
}}
>
{status.favourites_count}
</Text>
</CustomText>
) : null}
</>
)
@ -269,7 +270,7 @@ const TimelineActions: React.FC<Props> = ({
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
<View style={styles.actions}>
<View style={{ flexDirection: 'row' }}>
<Pressable
{...(highlighted
? {
@ -334,9 +335,6 @@ const TimelineActions: React.FC<Props> = ({
}
const styles = StyleSheet.create({
actions: {
flexDirection: 'row'
},
action: {
flex: 1,
flexDirection: 'row',

View File

@ -1,11 +1,12 @@
import analytics from '@components/analytics'
import Button from '@components/Button'
import openLink from '@components/openLink'
import CustomText from '@components/Text'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native'
import { View } from 'react-native'
import { Blurhash } from 'react-native-blurhash'
import attachmentAspectRatio from './aspectRatio'
@ -27,10 +28,14 @@ const AttachmentUnsupported: React.FC<Props> = ({
return (
<View
style={[
styles.base,
{ aspectRatio: attachmentAspectRatio({ total, index }) }
]}
style={{
flex: 1,
flexBasis: '50%',
padding: StyleConstants.Spacing.XS / 2,
justifyContent: 'center',
alignItems: 'center',
aspectRatio: attachmentAspectRatio({ total, index })
}}
>
{attachment.blurhash ? (
<Blurhash
@ -44,18 +49,18 @@ const AttachmentUnsupported: React.FC<Props> = ({
) : null}
{!sensitiveShown ? (
<>
<Text
style={[
styles.text,
{
color: attachment.blurhash
? colors.backgroundDefault
: colors.primaryDefault
}
]}
<CustomText
fontStyle='S'
style={{
textAlign: 'center',
marginBottom: StyleConstants.Spacing.S,
color: attachment.blurhash
? colors.backgroundDefault
: colors.primaryDefault
}}
>
{t('shared.attachment.unsupported.text')}
</Text>
</CustomText>
{attachment.remote_url ? (
<Button
type='text'
@ -74,19 +79,4 @@ const AttachmentUnsupported: React.FC<Props> = ({
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
flexBasis: '50%',
padding: StyleConstants.Spacing.XS / 2,
justifyContent: 'center',
alignItems: 'center'
},
text: {
...StyleConstants.FontStyle.S,
textAlign: 'center',
marginBottom: StyleConstants.Spacing.S
}
})
export default AttachmentUnsupported

View File

@ -1,6 +1,6 @@
import Button from '@components/Button'
import { StyleConstants } from '@utils/styles/constants'
import { Video } from 'expo-av'
import { ResizeMode, Video, VideoFullscreenUpdate } from 'expo-av'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import {
AppState,
@ -110,15 +110,14 @@ const AttachmentVideo: React.FC<Props> = ({
source: { uri: video.url }
}
: {
resizeMode: 'cover',
resizeMode: ResizeMode.COVER,
posterSource: { uri: video.preview_url },
posterStyle: { resizeMode: 'cover' }
posterStyle: { resizeMode: ResizeMode.COVER }
})}
useNativeControls={false}
onFullscreenUpdate={async event => {
if (
event.fullscreenUpdate ===
Video.FULLSCREEN_UPDATE_PLAYER_DID_DISMISS
event.fullscreenUpdate === VideoFullscreenUpdate.PLAYER_DID_DISMISS
) {
if (gifv) {
await videoPlayer.current?.pauseAsync()

View File

@ -1,11 +1,12 @@
import analytics from '@components/analytics'
import GracefullyImage from '@components/GracefullyImage'
import openLink from '@components/openLink'
import CustomText from '@components/Text'
import { useNavigation } from '@react-navigation/native'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { Pressable, StyleSheet, View } from 'react-native'
export interface Props {
card: Pick<
@ -22,7 +23,16 @@ const TimelineCard = React.memo(({ card }: Props) => {
<Pressable
accessible
accessibilityRole='link'
style={[styles.card, { borderColor: colors.border }]}
style={{
flex: 1,
flexDirection: 'row',
height: StyleConstants.Font.LineHeight.M * 5,
marginTop: StyleConstants.Spacing.M,
borderWidth: StyleSheet.hairlineWidth,
borderRadius: 6,
overflow: 'hidden',
borderColor: colors.border
}}
onPress={async () => {
analytics('timeline_shared_card_press')
await openLink(card.url, navigation)
@ -33,71 +43,46 @@ const TimelineCard = React.memo(({ card }: Props) => {
<GracefullyImage
uri={{ original: card.image }}
blurhash={card.blurhash}
style={styles.left}
imageStyle={styles.image}
style={{ flexBasis: StyleConstants.Font.LineHeight.M * 5 }}
imageStyle={{ borderTopLeftRadius: 6, borderBottomLeftRadius: 6 }}
/>
) : null}
<View style={styles.right}>
<Text
<View style={{ flex: 1, padding: StyleConstants.Spacing.S }}>
<CustomText
fontStyle='S'
numberOfLines={2}
style={[styles.rightTitle, { color: colors.primaryDefault }]}
style={{
marginBottom: StyleConstants.Spacing.XS,
fontWeight: StyleConstants.Font.Weight.Bold,
color: colors.primaryDefault
}}
testID='title'
>
{card.title}
</Text>
</CustomText>
{card.description ? (
<Text
<CustomText
fontStyle='S'
numberOfLines={1}
style={[styles.rightDescription, { color: colors.primaryDefault }]}
style={{
marginBottom: StyleConstants.Spacing.XS,
color: colors.primaryDefault
}}
testID='description'
>
{card.description}
</Text>
</CustomText>
) : null}
<Text
<CustomText
fontStyle='S'
numberOfLines={1}
style={[styles.rightLink, { color: colors.secondary }]}
style={{ color: colors.secondary }}
>
{card.url}
</Text>
</CustomText>
</View>
</Pressable>
)
})
const styles = StyleSheet.create({
card: {
flex: 1,
flexDirection: 'row',
height: StyleConstants.Font.LineHeight.M * 5,
marginTop: StyleConstants.Spacing.M,
borderWidth: StyleSheet.hairlineWidth,
borderRadius: 6,
overflow: 'hidden'
},
left: {
flexBasis: StyleConstants.Font.LineHeight.M * 5
},
image: {
borderTopLeftRadius: 6,
borderBottomLeftRadius: 6
},
right: {
flex: 1,
padding: StyleConstants.Spacing.S
},
rightTitle: {
...StyleConstants.FontStyle.S,
marginBottom: StyleConstants.Spacing.XS,
fontWeight: StyleConstants.Font.Weight.Bold
},
rightDescription: {
...StyleConstants.FontStyle.S,
marginBottom: StyleConstants.Spacing.XS
},
rightLink: {
...StyleConstants.FontStyle.S
}
})
export default TimelineCard

View File

@ -5,10 +5,10 @@ import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
export interface Props {
status: Pick<
Mastodon.Status,
'content' | 'spoiler_text' | 'emojis' | 'mentions' | 'tags'
>
status: Pick<Mastodon.Status, 'content' | 'spoiler_text' | 'emojis'> & {
mentions?: Mastodon.Status['mentions']
tags?: Mastodon.Status['tags']
}
numberOfLines?: number
highlighted?: boolean
disableDetails?: boolean

View File

@ -1,4 +1,5 @@
import analytics from '@components/analytics'
import CustomText from '@components/Text'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
@ -7,7 +8,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native'
import { StyleSheet, View } from 'react-native'
export interface Props {
status: Pick<
@ -37,7 +38,7 @@ const TimelineFeedback = React.memo(
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row' }}>
{status.reblogs_count > 0 ? (
<Text
<CustomText
accessibilityLabel={t(
'shared.actionsUsers.reblogged_by.accessibilityLabel',
{
@ -64,10 +65,10 @@ const TimelineFeedback = React.memo(
{t('shared.actionsUsers.reblogged_by.text', {
count: status.reblogs_count
})}
</Text>
</CustomText>
) : null}
{status.favourites_count > 0 ? (
<Text
<CustomText
accessibilityLabel={t(
'shared.actionsUsers.favourited_by.accessibilityLabel',
{
@ -94,12 +95,12 @@ const TimelineFeedback = React.memo(
{t('shared.actionsUsers.favourited_by.text', {
count: status.favourites_count
})}
</Text>
</CustomText>
) : null}
</View>
<View>
{data && data.length > 1 ? (
<Text
<CustomText
accessibilityLabel={t(
'shared.actionsUsers.history.accessibilityLabel',
{
@ -121,7 +122,7 @@ const TimelineFeedback = React.memo(
{t('shared.actionsUsers.history.text', {
count: data.length - 1
})}
</Text>
</CustomText>
) : null}
</View>
</View>

View File

@ -1,3 +1,4 @@
import CustomText from '@components/Text'
import { store } from '@root/store'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstance, getInstanceAccount } from '@utils/slices/instancesSlice'
@ -6,7 +7,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
import htmlparser2 from 'htmlparser2-without-node-native'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Text, View } from 'react-native'
import { View } from 'react-native'
const TimelineFiltered = React.memo(
() => {
@ -15,9 +16,9 @@ const TimelineFiltered = React.memo(
return (
<View style={{ backgroundColor: colors.backgroundDefault }}>
<Text
<CustomText
fontStyle='S'
style={{
...StyleConstants.FontStyle.S,
color: colors.secondary,
textAlign: 'center',
paddingVertical: StyleConstants.Spacing.S,
@ -25,7 +26,7 @@ const TimelineFiltered = React.memo(
}}
>
{t('shared.filtered')}
</Text>
</CustomText>
</View>
)
},

View File

@ -1,9 +1,9 @@
import CustomText from '@components/Text'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Text } from 'react-native'
export interface Props {
queryKey?: QueryKeyTimeline
@ -22,15 +22,15 @@ const TimelineFullConversation = React.memo(
status.mentions.filter(
mention => mention.id !== status.in_reply_to_account_id
).length) ? (
<Text
<CustomText
fontStyle='S'
style={{
...StyleConstants.FontStyle.S,
color: colors.blue,
marginTop: StyleConstants.Spacing.S
}}
>
{t('shared.fullConversation')}
</Text>
</CustomText>
) : null
},
() => true

View File

@ -2,6 +2,7 @@ import analytics from '@components/analytics'
import Icon from '@components/Icon'
import { displayMessage } from '@components/Message'
import { ParseEmojis } from '@components/Parse'
import CustomText from '@components/Text'
import {
QueryKeyTimeline,
useTimelineMutation
@ -10,7 +11,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, Text, View } from 'react-native'
import { Pressable, View } from 'react-native'
import { useQueryClient } from 'react-query'
import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted'
@ -20,22 +21,22 @@ const Names = ({ accounts }: { accounts: Mastodon.Account[] }) => {
const { colors } = useTheme()
return (
<Text
<CustomText
numberOfLines={1}
style={{ ...StyleConstants.FontStyle.M, color: colors.secondary }}
>
<Text>{t('shared.header.conversation.withAccounts')}</Text>
<CustomText>{t('shared.header.conversation.withAccounts')}</CustomText>
{accounts.map((account, index) => (
<Text key={account.id} numberOfLines={1}>
<CustomText key={account.id} numberOfLines={1}>
{index !== 0 ? t('common:separator') : undefined}
<ParseEmojis
content={account.display_name || account.username}
emojis={account.emojis}
fontBold
/>
</Text>
</CustomText>
))}
</Text>
</CustomText>
)
}

View File

@ -18,9 +18,15 @@ export interface Props {
queryKey?: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
status: Mastodon.Status
highlighted: boolean
}
const TimelineHeaderDefault = ({ queryKey, rootQueryKey, status }: Props) => {
const TimelineHeaderDefault = ({
queryKey,
rootQueryKey,
status,
highlighted
}: Props) => {
const { t } = useTranslation('componentTimeline')
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
const { colors } = useTheme()
@ -40,6 +46,7 @@ const TimelineHeaderDefault = ({ queryKey, rootQueryKey, status }: Props) => {
<HeaderSharedCreated
created_at={status.created_at}
edited_at={status.edited_at}
highlighted={highlighted}
/>
<HeaderSharedVisibility visibility={status.visibility} />
<HeaderSharedMuted muted={status.muted} />

View File

@ -1,9 +1,10 @@
import CustomText from '@components/Text'
import { ParseEmojis } from '@root/components/Parse'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native'
import { View } from 'react-native'
export interface Props {
account: Mastodon.Account
@ -16,13 +17,13 @@ const HeaderSharedAccount = React.memo(
const { colors } = useTheme()
return (
<View style={styles.base}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{withoutName ? null : (
<Text
<CustomText
accessibilityHint={t(
'shared.header.shared.account.name.accessibilityHint'
)}
style={styles.name}
style={{ marginRight: StyleConstants.Spacing.XS }}
numberOfLines={1}
>
<ParseEmojis
@ -30,34 +31,21 @@ const HeaderSharedAccount = React.memo(
emojis={account.emojis}
fontBold
/>
</Text>
</CustomText>
)}
<Text
<CustomText
accessibilityHint={t(
'shared.header.shared.account.account.accessibilityHint'
)}
style={[styles.acct, { color: colors.secondary }]}
style={{ flexShrink: 1, color: colors.secondary }}
numberOfLines={1}
>
@{account.acct}
</Text>
</CustomText>
</View>
)
},
() => true
)
const styles = StyleSheet.create({
base: {
flexDirection: 'row',
alignItems: 'center'
},
name: {
marginRight: StyleConstants.Spacing.XS
},
acct: {
flexShrink: 1
}
})
export default HeaderSharedAccount

View File

@ -1,10 +1,10 @@
import analytics from '@components/analytics'
import openLink from '@components/openLink'
import CustomText from '@components/Text'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text } from 'react-native'
export interface Props {
application?: Mastodon.Application
@ -16,7 +16,8 @@ const HeaderSharedApplication = React.memo(
const { t } = useTranslation('componentTimeline')
return application && application.name !== 'Web' ? (
<Text
<CustomText
fontStyle='S'
accessibilityRole='link'
onPress={async () => {
analytics('timeline_shared_header_application_press', {
@ -24,24 +25,20 @@ const HeaderSharedApplication = React.memo(
})
application.website && (await openLink(application.website))
}}
style={[styles.application, { color: colors.secondary }]}
style={{
flex: 1,
marginLeft: StyleConstants.Spacing.S,
color: colors.secondary
}}
numberOfLines={1}
>
{t('shared.header.shared.application', {
application: application.name
})}
</Text>
</CustomText>
) : null
},
() => true
)
const styles = StyleSheet.create({
application: {
flex: 1,
...StyleConstants.FontStyle.S,
marginLeft: StyleConstants.Spacing.S
}
})
export default HeaderSharedApplication

View File

@ -1,28 +1,44 @@
import Icon from '@components/Icon'
import RelativeTime from '@components/RelativeTime'
import CustomText from '@components/Text'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Text } from 'react-native'
import { FormattedDate, FormattedRelativeTime, FormattedTime } from 'react-intl'
export interface Props {
created_at: Mastodon.Status['created_at']
created_at: Mastodon.Status['created_at'] | number
edited_at?: Mastodon.Status['edited_at']
highlighted?: boolean
}
const HeaderSharedCreated = React.memo(
({ created_at, edited_at }: Props) => {
({ created_at, edited_at, highlighted = false }: Props) => {
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const actualTime = edited_at || created_at
return (
<>
<Text
style={{ ...StyleConstants.FontStyle.S, color: colors.secondary }}
>
<RelativeTime date={edited_at || created_at} />
</Text>
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
{highlighted ? (
<>
<FormattedDate
value={new Date(actualTime)}
dateStyle='medium'
timeStyle='short'
/>
</>
) : (
<FormattedRelativeTime
value={
-(new Date().getTime() - new Date(actualTime).getTime()) / 1000
}
updateIntervalInSeconds={1}
/>
)}
</CustomText>
{edited_at ? (
<Icon
accessibilityLabel={t(

View File

@ -15,6 +15,30 @@ const HeaderSharedVisibility = React.memo(
const { colors } = useTheme()
switch (visibility) {
case 'public':
return (
<Icon
accessibilityLabel={t(
'shared.header.shared.visibility.private.accessibilityLabel'
)}
name='Globe'
size={StyleConstants.Font.Size.S}
color={colors.secondary}
style={styles.visibility}
/>
)
case 'unlisted':
return (
<Icon
accessibilityLabel={t(
'shared.header.shared.visibility.private.accessibilityLabel'
)}
name='Unlock'
size={StyleConstants.Font.Size.S}
color={colors.secondary}
style={styles.visibility}
/>
)
case 'private':
return (
<Icon

View File

@ -4,7 +4,7 @@ import haptics from '@components/haptics'
import Icon from '@components/Icon'
import { displayMessage } from '@components/Message'
import { ParseEmojis } from '@components/Parse'
import RelativeTime from '@components/RelativeTime'
import CustomText from '@components/Text'
import {
MutationVarsTimelineUpdateStatusProperty,
QueryKeyTimeline,
@ -16,6 +16,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
import { maxBy } from 'lodash'
import React, { useCallback, useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { FormattedRelativeTime } from 'react-intl'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { useQueryClient } from 'react-query'
@ -83,7 +84,7 @@ const TimelinePoll: React.FC<Props> = ({
if (!poll.expired) {
if (!sameAccount && !poll.voted) {
return (
<View style={styles.button}>
<View style={{ marginRight: StyleConstants.Spacing.S }}>
<Button
onPress={() => {
analytics('timeline_shared_vote_vote_press')
@ -110,7 +111,7 @@ const TimelinePoll: React.FC<Props> = ({
)
} else {
return (
<View style={styles.button}>
<View style={{ marginRight: StyleConstants.Spacing.S }}>
<Button
onPress={() => {
analytics('timeline_shared_vote_refresh_press')
@ -147,19 +148,28 @@ const TimelinePoll: React.FC<Props> = ({
const pollExpiration = useMemo(() => {
if (poll.expired) {
return (
<Text style={[styles.expiration, { color: colors.secondary }]}>
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
{t('shared.poll.meta.expiration.expired')}
</Text>
</CustomText>
)
} else {
if (poll.expires_at) {
return (
<Text style={[styles.expiration, { color: colors.secondary }]}>
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
<Trans
i18nKey='componentTimeline:shared.poll.meta.expiration.until'
components={[<RelativeTime date={poll.expires_at} />]}
components={[
<FormattedRelativeTime
value={
(new Date(poll.expires_at).getTime() -
new Date().getTime()) /
1000
}
updateIntervalInSeconds={1}
/>
]}
/>
</Text>
</CustomText>
)
}
}
@ -179,10 +189,17 @@ const TimelinePoll: React.FC<Props> = ({
option => option.votes_count
)?.votes_count
return poll.options.map((option, index) => (
<View key={index} style={styles.optionContainer}>
<View style={styles.optionContent}>
<View
key={index}
style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}
>
<View style={{ flex: 1, flexDirection: 'row' }}>
<Icon
style={styles.optionSelection}
style={{
paddingTop:
StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M,
marginRight: StyleConstants.Spacing.S
}}
name={
`${poll.own_votes?.includes(index) ? 'Check' : ''}${
poll.multiple ? 'Square' : 'Circle'
@ -193,11 +210,18 @@ const TimelinePoll: React.FC<Props> = ({
poll.own_votes?.includes(index) ? colors.blue : colors.disabled
}
/>
<Text style={styles.optionText}>
<CustomText style={{ flex: 1 }}>
<ParseEmojis content={option.title} emojis={poll.emojis} />
</Text>
<Text
style={[styles.optionPercentage, { color: colors.primaryDefault }]}
</CustomText>
<CustomText
fontStyle='M'
style={{
alignSelf: 'center',
marginLeft: StyleConstants.Spacing.S,
flexBasis: '20%',
textAlign: 'center',
color: colors.primaryDefault
}}
>
{poll.votes_count
? Math.round(
@ -207,21 +231,24 @@ const TimelinePoll: React.FC<Props> = ({
)
: 0}
%
</Text>
</CustomText>
</View>
<View
style={[
styles.background,
{
width: `${Math.round(
(option.votes_count / (poll.voters_count || poll.votes_count)) *
100
)}%`,
backgroundColor:
option.votes_count === maxValue ? colors.blue : colors.disabled
}
]}
style={{
height: StyleConstants.Spacing.XS,
minWidth: 2,
borderTopRightRadius: 10,
borderBottomRightRadius: 10,
marginTop: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.S,
width: `${Math.round(
(option.votes_count / (poll.voters_count || poll.votes_count)) *
100
)}%`,
backgroundColor:
option.votes_count === maxValue ? colors.blue : colors.disabled
}}
/>
</View>
))
@ -230,7 +257,7 @@ const TimelinePoll: React.FC<Props> = ({
return poll.options.map((option, index) => (
<Pressable
key={index}
style={styles.optionContainer}
style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}
onPress={() => {
analytics('timeline_shared_vote_option_press')
!allOptions[index] && haptics('Light')
@ -253,16 +280,20 @@ const TimelinePoll: React.FC<Props> = ({
}
}}
>
<View style={[styles.optionContent]}>
<View style={{ flex: 1, flexDirection: 'row' }}>
<Icon
style={styles.optionSelection}
style={{
paddingTop:
StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M,
marginRight: StyleConstants.Spacing.S
}}
name={isSelected(index)}
size={StyleConstants.Font.Size.M}
color={colors.primaryDefault}
/>
<Text style={styles.optionText}>
<CustomText style={{ flex: 1 }}>
<ParseEmojis content={option.title} emojis={poll.emojis} />
</Text>
</CustomText>
</View>
</Pressable>
))
@ -271,25 +302,32 @@ const TimelinePoll: React.FC<Props> = ({
const pollVoteCounts = useMemo(() => {
if (poll.voters_count !== null) {
return (
<Text style={[styles.votes, { color: colors.secondary }]}>
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
{t('shared.poll.meta.count.voters', { count: poll.voters_count })}
{' • '}
</Text>
</CustomText>
)
} else if (poll.votes_count !== null) {
return (
<Text style={[styles.votes, { color: colors.secondary }]}>
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
{t('shared.poll.meta.count.votes', { count: poll.votes_count })}
{' • '}
</Text>
</CustomText>
)
}
}, [poll.voters_count, poll.votes_count])
return (
<View style={styles.base}>
<View style={{ marginTop: StyleConstants.Spacing.M }}>
{poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow}
<View style={styles.meta}>
<View
style={{
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginTop: StyleConstants.Spacing.XS
}}
>
{pollButton}
{pollVoteCounts}
{pollExpiration}
@ -298,55 +336,4 @@ const TimelinePoll: React.FC<Props> = ({
)
}
const styles = StyleSheet.create({
base: {
marginTop: StyleConstants.Spacing.M
},
optionContainer: {
flex: 1,
paddingVertical: StyleConstants.Spacing.S
},
optionContent: {
flex: 1,
flexDirection: 'row'
},
optionText: {
flex: 1
},
optionSelection: {
paddingTop: StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M,
marginRight: StyleConstants.Spacing.S
},
optionPercentage: {
...StyleConstants.FontStyle.M,
alignSelf: 'center',
marginLeft: StyleConstants.Spacing.S,
flexBasis: '20%',
textAlign: 'center'
},
background: {
height: StyleConstants.Spacing.XS,
minWidth: 2,
borderTopRightRadius: 10,
borderBottomRightRadius: 10,
marginTop: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.S
},
meta: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginTop: StyleConstants.Spacing.XS
},
button: {
marginRight: StyleConstants.Spacing.S
},
votes: {
...StyleConstants.FontStyle.S
},
expiration: {
...StyleConstants.FontStyle.S
}
})
export default TimelinePoll

View File

@ -1,5 +1,6 @@
import analytics from '@components/analytics'
import { ParseHTML } from '@components/Parse'
import CustomText from '@components/Text'
import { useTranslateQuery } from '@utils/queryHooks/translate'
import { getSettingsLanguage } from '@utils/slices/settingsSlice'
import { StyleConstants } from '@utils/styles/constants'
@ -7,7 +8,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
import * as Localization from 'expo-localization'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, Text } from 'react-native'
import { Pressable } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import { useSelector } from 'react-redux'
@ -85,9 +86,9 @@ const TimelineTranslate = React.memo(
}
}}
>
<Text
<CustomText
fontStyle='M'
style={{
...StyleConstants.FontStyle.M,
color:
isLoading || isSuccess
? colors.secondary
@ -106,14 +107,14 @@ const TimelineTranslate = React.memo(
source: data?.sourceLanguage
})
: t('shared.translate.default')}
</Text>
<Text>
</CustomText>
<CustomText>
{__DEV__
? ` Source: ${status.language}; Target: ${
Localization.locale || settingsLanguage || 'en'
}`
: undefined}
</Text>
</CustomText>
{isLoading ? (
<Circle
size={StyleConstants.Font.Size.M}

View File

@ -2,7 +2,10 @@ import analytics from '@components/analytics'
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
import * as ImageManipulator from 'expo-image-manipulator'
import * as ImagePicker from 'expo-image-picker'
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
import {
ImageInfo,
UIImagePickerPresentationStyle
} from 'expo-image-picker/build/ImagePicker.types'
import i18next from 'i18next'
import { Alert, Linking, Platform } from 'react-native'
@ -39,7 +42,7 @@ const mediaSelector = async ({
{ resize }
])
}
resolve(newResult)
resolve({ ...newResult, cancelled: false })
} else {
resolve(result)
}
@ -94,8 +97,8 @@ const mediaSelector = async ({
exif: false,
presentationStyle:
Platform.OS === 'ios' && parseInt(Platform.Version) < 13
? 0
: -2
? UIImagePickerPresentationStyle.FULL_SCREEN
: UIImagePickerPresentationStyle.AUTOMATIC
})
if (!result.cancelled) {

View File

@ -34,6 +34,7 @@ const openLink = async (url: string, navigation?: any) => {
// @ts-ignore
navigation.push(page, options)
} else {
// @ts-ignore
navigationRef.navigate(page, options)
}
}

View File

@ -1,20 +0,0 @@
{
"strings": {
"prefixAgo": "",
"prefixFromNow": "",
"suffixAgo": "her",
"suffixFromNow": "",
"seconds": "%d Sekunden",
"minute": "etwa eine Minute",
"minutes": "%d Minuten",
"hour": "etwa eine Stunde",
"hours": "etwa %d Stunden",
"day": "1 Tag",
"days": "%d Tage",
"month": "etwa 1 Monat",
"months": "%d Monate",
"year": "etwa 1 Jahr",
"years": "%d Jahre",
"wordSeparator": ""
}
}

View File

@ -62,8 +62,8 @@
"history": {
"accessibilityLabel": "Dieser Tröt wurde {{count}} mal bearbeitet",
"accessibilityHint": "Für den vollständigen Verlauf auswählen",
"text": "{{count}} bearbeitet",
"text_plural": "{{count}} mal bearbeitet"
"text_one": "{{count}} bearbeitet",
"text_other": "{{count}} mal bearbeitet"
}
},
"attachment": {
@ -219,10 +219,10 @@
"refresh": "Aktualisieren"
},
"count": {
"voters": "{{count}} Benutzer haben abgestimmt",
"voters_plural": "{{count}} Benutzer haben abgestimmt",
"votes": "{{count}} Stimmen",
"votes_plural": "{{count}} Stimmen"
"voters_one": "{{count}} Benutzer haben abgestimmt",
"voters_other": "{{count}} Benutzer haben abgestimmt",
"votes_one": "{{count}} Stimmen",
"votes_other": "{{count}} Stimmen"
},
"expiration": {
"expired": "Abstimmung abgelaufen",

View File

@ -13,6 +13,5 @@ export default {
componentMediaSelector: require('./components/mediaSelector'),
componentParse: require('./components/parse'),
componentRelationship: require('./components/relationship'),
componentRelativeTime: require('./components/relativeTime'),
componentTimeline: require('./components/timeline')
}

View File

@ -1,5 +1,6 @@
{
"buttons": {
"OK": "OK",
"apply": "Apply",
"cancel": "Cancel"
},

View File

@ -1,20 +0,0 @@
{
"strings": {
"prefixAgo": "",
"prefixFromNow": "",
"suffixAgo": "ago",
"suffixFromNow": "",
"seconds": "%d seconds",
"minute": "about a minute",
"minutes": "%d minutes",
"hour": "about an hour",
"hours": "about %d hours",
"day": "a day",
"days": "%d days",
"month": "about a month",
"months": "%d months",
"year": "about a year",
"years": "%d years",
"wordSeparator": " "
}
}

View File

@ -62,8 +62,8 @@
"history": {
"accessibilityLabel": "This toot has been edited {{count}} times",
"accessibilityHint": "Tap to view the full edit history",
"text": "{{count}} edit",
"text_plural": "{{count}} edits"
"text_one": "{{count}} edit",
"text_other": "{{count}} edits"
}
},
"attachment": {
@ -219,10 +219,10 @@
"refresh": "Refresh"
},
"count": {
"voters": "{{count}} user voted",
"voters_plural": "{{count}} users voted",
"votes": "{{count}} vote",
"votes_plural": "{{count}} votes"
"voters_one": "{{count}} user voted",
"voters_other": "{{count}} users voted",
"votes_one": "{{count}} vote",
"votes_other": "{{count}} votes"
},
"expiration": {
"expired": "Vote expired",

View File

@ -10,5 +10,9 @@
"pushError": {
"message": "Push service error",
"description": "Please re-enable push notification in settings"
},
"shareError": {
"imageNotSupported": "Image type {{type}} not supported",
"videoNotSupported": "Video type {{type}} not supported"
}
}

View File

@ -42,7 +42,13 @@
"placeholder": "Spoiler warning message"
},
"textInput": {
"placeholder": "What's on your mind"
"placeholder": "What's on your mind",
"keyboardImage": {
"exceedMaximum": {
"title": "Maximum attachments amount reached",
"OK": "$t(common:buttons.OK)"
}
}
}
},
"footer": {
@ -136,8 +142,8 @@
"accessibilityHint": "Open emoji selection panel, swipe horizontally to change page"
}
},
"drafts": "Draft ({{count}})",
"drafts_plural": "Drafts ({{count}})"
"drafts_one": "Draft ({{count}})",
"drafts_other": "Drafts ({{count}})"
},
"editAttachment": {
"header": {
@ -165,7 +171,8 @@
"content": {
"accessibilityHint": "Saved draft, tap to edit this draft",
"textEmpty": "Content empty"
}
},
"checkAttachment": "Checking attachments on the server..."
}
}
}

View File

@ -116,8 +116,8 @@
},
"fields": {
"title": "Metadata",
"total": "{{count}} field",
"total_plural": "{{count}} fields"
"total_one": "{{count}} field",
"total_other": "{{count}} fields"
},
"visibility": {
"title": "Posting Visibility",
@ -281,6 +281,7 @@
"accessibilityLabel": "Actions for user {{user}}",
"accessibilityHint": "You can mute, block, report or share this user"
},
"followed_by": " is following you",
"moved": "User moved",
"created_at": "Registered on: {{date}}",
"summary": {

View File

@ -13,6 +13,5 @@ export default {
componentMediaSelector: require('./components/mediaSelector'),
componentParse: require('./components/parse'),
componentRelationship: require('./components/relationship'),
componentRelativeTime: require('./components/relativeTime'),
componentTimeline: require('./components/timeline')
}

View File

@ -1,20 +0,0 @@
{
"strings": {
"prefixAgo": "",
"prefixFromNow": "",
"suffixAgo": "전",
"suffixFromNow": "",
"seconds": "%d초",
"minute": "약 1분",
"minutes": "%d분",
"hour": "약 1시간",
"hours": "약 %d시간",
"day": "하루",
"days": "%d일",
"month": "약 1달",
"months": "%d달",
"year": "약 1년",
"years": "%d년",
"wordSeparator": ""
}
}

View File

@ -196,10 +196,10 @@
"refresh": "새로고침"
},
"count": {
"voters": "{{count}}명의 사용자가 투표",
"voters_plural": "{{count}}명의 사용자가 투표",
"votes": "{{count}} 투표",
"votes_plural": "{{count}} 투표"
"voters_one": "{{count}}명의 사용자가 투표",
"voters_other": "{{count}}명의 사용자가 투표",
"votes_one": "{{count}} 투표",
"votes_other": "{{count}} 투표"
},
"expiration": {
"expired": "투표 종료됨",

View File

@ -134,8 +134,8 @@
"accessibilityHint": "이모지 선택 패널 열기, 가로로 스와이프해서 페이지를 바꿀 수 있어요"
}
},
"drafts": "초안 ({{count}})",
"drafts_plural": "초안 ({{count}})"
"drafts_one": "초안 ({{count}})",
"drafts_other": "초안 ({{count}})"
},
"editAttachment": {
"header": {

View File

@ -113,8 +113,8 @@
},
"fields": {
"title": "메타데이터",
"total": "{{count}}개 필드",
"total_plural": "{{count}}개 필드"
"total_one": "{{count}}개 필드",
"total_other": "{{count}}개 필드"
},
"visibility": {
"title": "공개 범위",

View File

@ -13,6 +13,5 @@ export default {
componentMediaSelector: require('./components/mediaSelector'),
componentParse: require('./components/parse'),
componentRelationship: require('./components/relationship'),
componentRelativeTime: require('./components/relativeTime'),
componentTimeline: require('./components/timeline')
}

View File

@ -1,5 +1,6 @@
{
"buttons": {
"OK": "OK",
"apply": "Áp dụng",
"cancel": "Hủy bỏ"
},

View File

@ -1,20 +0,0 @@
{
"strings": {
"prefixAgo": "",
"prefixFromNow": "",
"suffixAgo": " trước",
"suffixFromNow": "",
"seconds": "%d giây",
"minute": "một phút",
"minutes": "%d phút",
"hour": "một giờ",
"hours": "khoảng %d giờ",
"day": "một ngày",
"days": "%d ngày",
"month": "khoảng một tháng",
"months": "%d tháng",
"year": "khoảng một năm",
"years": "%d năm",
"wordSeparator": ""
}
}

View File

@ -58,6 +58,12 @@
"accessibilityLabel": "{{count}} người đã thích tút này",
"accessibilityHint": "Xem những ai thích",
"text": "$t(screenTabs:shared.users.statuses.favourited_by)"
},
"history": {
"accessibilityLabel": "Tút này đã sửa {{count}} lần",
"accessibilityHint": "Nhấn để xem lịch sử chỉnh sửa",
"text_one": "{{count}} lượt sửa",
"text_other": "{{count}} lượt sửa"
}
},
"attachment": {
@ -96,6 +102,9 @@
}
},
"application": "via {{application}}",
"edited": {
"accessibilityLabel": "Tút đã sửa"
},
"muted": {
"accessibilityLabel": "Đã ẩn tút"
},
@ -158,18 +167,30 @@
},
"status": {
"heading": "Đối với tút",
"edit": {
"function": "Sửa tút",
"button": "Sửa tút này"
},
"delete": {
"function": "Xóa tút",
"button": "Xóa tút này"
"button": "Xóa tút này",
"alert": {
"title": "Vẫn xóa tút?",
"message": "Bạn có chắc muốn xóa tút này? Toàn bộ lượt thích, đăng lại và trả lời tút cũng sẽ bị xóa theo.",
"buttons": {
"confirm": "Xác nhận xóa",
"cancel": "$t(common:buttons.cancel)"
}
}
},
"deleteEdit": {
"function": "Xóa tút",
"button": "Xóa và viết lại",
"button": "Xóa & viết lại",
"alert": {
"title": "Tiếp tục xóa tút?",
"title": "Xác nhận xóa tút?",
"message": "Bạn có chắc muốn xóa và viết lại tút này? Toàn bộ lượt thích, đăng lại và trả lời tút cũng sẽ bị xóa theo.",
"buttons": {
"confirm": "Tiếp tục xóa",
"confirm": "Xác nhận xóa",
"cancel": "$t(common:buttons.cancel)"
}
}
@ -198,10 +219,10 @@
"refresh": "Làm mới"
},
"count": {
"voters": "{{count}} người bình chọn",
"voters_plural": "{{count}} người bình chọn",
"votes": "{{count}} bình chọn",
"votes_plural": "{{count}} bình chọn"
"voters_one": "{{count}} người bình chọn",
"voters_other": "{{count}} người bình chọn",
"votes_one": "{{count}} bình chọn",
"votes_other": "{{count}} bình chọn"
},
"expiration": {
"expired": "Đã kết thúc",

View File

@ -10,5 +10,9 @@
"pushError": {
"message": "Lỗi thông báo đẩy",
"description": "Hãy cho phép thông báo đẩy trong mục cài đặt!"
},
"shareError": {
"imageNotSupported": "Không hỗ trợ định dạng ảnh {{type}}",
"videoNotSupported": "Không hỗ trợ định dạng video {{type}}"
}
}

View File

@ -16,7 +16,9 @@
"default": "Đăng",
"conversation": "Tin nhắn",
"reply": "Trả lời",
"edit": "Sửa"
"deleteEdit": "Tút",
"edit": "Sửa",
"share": "Tút"
},
"alert": {
"default": {
@ -40,7 +42,13 @@
"placeholder": "Viết nội dung ẩn của bạn ở đây"
},
"textInput": {
"placeholder": "Bạn đang nghĩ về điều gì?"
"placeholder": "Bạn đang nghĩ về điều gì?",
"keyboardImage": {
"exceedMaximum": {
"title": "Vượt quá số lượng tập tin cho phép",
"OK": "$t(common:buttons.OK)"
}
}
}
},
"footer": {
@ -134,8 +142,8 @@
"accessibilityHint": "Mở bảng chọn emoji, vuốt qua lại để xem toàn bộ"
}
},
"drafts": "Nháp ({{count}})",
"drafts_plural": "Nháp ({{count}})"
"drafts_one": "Nháp ({{count}})",
"drafts_other": "Nháp ({{count}})"
},
"editAttachment": {
"header": {
@ -163,7 +171,8 @@
"content": {
"accessibilityHint": "Đã lưu nháp, nhấn để tiếp tục viết",
"textEmpty": "Chưa có nội dung"
}
},
"checkAttachment": "Đang kiểm tra tập tin đính kèm trên máy chủ..."
}
}
}

View File

@ -116,8 +116,8 @@
},
"fields": {
"title": "Metadata",
"total": "{{count}} mục",
"total_plural": "{{count}} mục"
"total_one": "{{count}} mục",
"total_other": "{{count}} mục"
},
"visibility": {
"title": "Kiểu tút mặc định",
@ -267,7 +267,8 @@
"heading": "Thu thập dữ liệu",
"description": "Giúp cải thiện chất lượng app"
},
"version": "Phiên bản {{version}}"
"version": "Phiên bản {{version}}",
"instanceVersion": "Mastodon phiên bản {{version}}"
},
"switch": {
"existing": "Đã đăng nhập trước đây",
@ -280,6 +281,7 @@
"accessibilityLabel": "Hành động đối với {{user}}",
"accessibilityHint": "Bạn có thể ẩn, chặn, báo cáo hoặc chia sẻ người này"
},
"followed_by": " đang theo dõi bạn",
"moved": "Đã chuyển đi",
"created_at": "Đã tham gia {{date}}",
"summary": {
@ -331,6 +333,9 @@
"reblogged_by": "{{count}} đăng lại",
"favourited_by": "{{count}} thích"
}
},
"history": {
"name": "Lịch sử chỉnh sửa"
}
}
}

View File

@ -13,6 +13,5 @@ export default {
componentMediaSelector: require('./components/mediaSelector'),
componentParse: require('./components/parse'),
componentRelationship: require('./components/relationship'),
componentRelativeTime: require('./components/relativeTime'),
componentTimeline: require('./components/timeline')
}

Some files were not shown because too many files have changed in this diff Show More