Merge branch 'main' into upload-image
commit
c5f3200d6b
|
@ -31,6 +31,11 @@ describe('extractEntities', () => {
|
|||
'start middle end.com/foo/bar?baz=bux#hash',
|
||||
'newline1.com\nnewline2.com',
|
||||
'not.. a..url ..here',
|
||||
'e.g.',
|
||||
'something-cool.jpg',
|
||||
'website.com.jpg',
|
||||
'e.g./foo',
|
||||
'website.com.jpg/foo',
|
||||
]
|
||||
interface Output {
|
||||
type: string
|
||||
|
@ -80,6 +85,11 @@ describe('extractEntities', () => {
|
|||
{type: 'link', value: 'newline2.com', noScheme: true},
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
]
|
||||
it('correctly handles a set of text inputs', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
|
@ -145,6 +155,12 @@ describe('detectLinkables', () => {
|
|||
'start middle end.com/foo/bar?baz=bux#hash',
|
||||
'newline1.com\nnewline2.com',
|
||||
'not.. a..url ..here',
|
||||
'e.g.',
|
||||
'e.g. real.com fake.notreal',
|
||||
'something-cool.jpg',
|
||||
'website.com.jpg',
|
||||
'e.g./foo',
|
||||
'website.com.jpg/foo',
|
||||
]
|
||||
const outputs = [
|
||||
['no linkable'],
|
||||
|
@ -171,6 +187,12 @@ describe('detectLinkables', () => {
|
|||
['start middle ', {link: 'end.com/foo/bar?baz=bux#hash'}],
|
||||
[{link: 'newline1.com'}, '\n', {link: 'newline2.com'}],
|
||||
['not.. a..url ..here'],
|
||||
['e.g.'],
|
||||
['e.g. ', {link: 'real.com'}, ' fake.notreal'],
|
||||
['something-cool.jpg'],
|
||||
['website.com.jpg'],
|
||||
['e.g./foo'],
|
||||
['website.com.jpg/foo'],
|
||||
]
|
||||
it('correctly handles a set of text inputs', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
|
|
|
@ -48,7 +48,8 @@
|
|||
"react-native-svg": "^12.4.0",
|
||||
"react-native-tab-view": "^3.3.0",
|
||||
"react-native-url-polyfill": "^1.3.0",
|
||||
"react-native-web": "^0.17.7"
|
||||
"react-native-web": "^0.17.7",
|
||||
"tlds": "^1.234.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.9",
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, {useState, useEffect} from 'react'
|
|||
import * as view from './view/index'
|
||||
import {RootStoreModel, setupState, RootStoreProvider} from './state'
|
||||
import {DesktopWebShell} from './view/shell/desktop-web'
|
||||
import Toast from './view/com/util/Toast'
|
||||
import Toast from 'react-native-root-toast'
|
||||
|
||||
function App() {
|
||||
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {AtUri} from '../third-party/uri'
|
||||
import {Entity} from '../third-party/api/src/client/types/app/bsky/feed/post'
|
||||
import {PROD_SERVICE} from '../state'
|
||||
import TLDs from 'tlds'
|
||||
|
||||
export const MAX_DISPLAY_NAME = 64
|
||||
export const MAX_DESCRIPTION = 256
|
||||
|
@ -57,6 +58,14 @@ export function ago(date: number | string | Date): string {
|
|||
}
|
||||
}
|
||||
|
||||
export function isValidDomain(str: string): boolean {
|
||||
return !!TLDs.find(tld => {
|
||||
let i = str.lastIndexOf(tld)
|
||||
if (i === -1) return false
|
||||
return str.charAt(i - 1) === '.' && i === str.length - tld.length
|
||||
})
|
||||
}
|
||||
|
||||
export function extractEntities(
|
||||
text: string,
|
||||
knownHandles?: Set<string>,
|
||||
|
@ -85,10 +94,14 @@ export function extractEntities(
|
|||
{
|
||||
// links
|
||||
const re =
|
||||
/(^|\s)((https?:\/\/[\S]+)|([a-z][a-z0-9]*(\.[a-z0-9]+)+[\S]*))(\b)/dg
|
||||
/(^|\s)((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))(\b)/dg
|
||||
while ((match = re.exec(text))) {
|
||||
let value = match[2]
|
||||
if (!value.startsWith('http')) {
|
||||
const domain = match.groups?.domain
|
||||
if (!domain || !isValidDomain(domain)) {
|
||||
continue
|
||||
}
|
||||
value = `https://${value}`
|
||||
}
|
||||
ents.push({
|
||||
|
@ -110,7 +123,7 @@ interface DetectedLink {
|
|||
type DetectedLinkable = string | DetectedLink
|
||||
export function detectLinkables(text: string): DetectedLinkable[] {
|
||||
const re =
|
||||
/((^|\s)@[a-z0-9\.-]*)|((^|\s)https?:\/\/[\S]+)|((^|\s)[a-z][a-z0-9]*(\.[a-z0-9]+)+[\S]*)/gi
|
||||
/((^|\s)@[a-z0-9\.-]*)|((^|\s)https?:\/\/[\S]+)|((^|\s)(?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*)/gi
|
||||
const segments = []
|
||||
let match
|
||||
let start = 0
|
||||
|
@ -118,6 +131,10 @@ export function detectLinkables(text: string): DetectedLinkable[] {
|
|||
let matchIndex = match.index
|
||||
let matchValue = match[0]
|
||||
|
||||
if (match.groups?.domain && !isValidDomain(match.groups?.domain)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (/\s/.test(matchValue)) {
|
||||
// HACK
|
||||
// skip the starting space
|
||||
|
|
|
@ -7,6 +7,8 @@ import * as apilib from '../lib/api'
|
|||
import {cleanError} from '../../lib/strings'
|
||||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
|
||||
type FeedItem = GetTimeline.FeedItem | GetAuthorFeed.FeedItem
|
||||
type FeedItemWithThreadMeta = FeedItem & {
|
||||
_isThreadParent?: boolean
|
||||
|
@ -166,6 +168,7 @@ export class FeedModel {
|
|||
params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams
|
||||
hasMore = true
|
||||
loadMoreCursor: string | undefined
|
||||
pollCursor: string | undefined
|
||||
_loadPromise: Promise<void> | undefined
|
||||
_loadMorePromise: Promise<void> | undefined
|
||||
_loadLatestPromise: Promise<void> | undefined
|
||||
|
@ -300,7 +303,7 @@ export class FeedModel {
|
|||
const res = await this._getFeed({limit: 1})
|
||||
this.setHasNewLatest(
|
||||
res.data.feed[0] &&
|
||||
(this.feed.length === 0 || res.data.feed[0].uri !== this.feed[0]?.uri),
|
||||
(this.feed.length === 0 || res.data.feed[0].uri !== this.pollCursor),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -341,7 +344,7 @@ export class FeedModel {
|
|||
private async _initialLoad(isRefreshing = false) {
|
||||
this._xLoading(isRefreshing)
|
||||
try {
|
||||
const res = await this._getFeed()
|
||||
const res = await this._getFeed({limit: PAGE_SIZE})
|
||||
this._replaceAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
|
@ -352,7 +355,7 @@ export class FeedModel {
|
|||
private async _loadLatest() {
|
||||
this._xLoading()
|
||||
try {
|
||||
const res = await this._getFeed()
|
||||
const res = await this._getFeed({limit: PAGE_SIZE})
|
||||
this._prependAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
|
@ -368,6 +371,7 @@ export class FeedModel {
|
|||
try {
|
||||
const res = await this._getFeed({
|
||||
before: this.loadMoreCursor,
|
||||
limit: PAGE_SIZE,
|
||||
})
|
||||
this._appendAll(res)
|
||||
this._xIdle()
|
||||
|
@ -402,6 +406,7 @@ export class FeedModel {
|
|||
|
||||
private _replaceAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
|
||||
this.feed.length = 0
|
||||
this.pollCursor = res.data.feed[0]?.uri
|
||||
this._appendAll(res)
|
||||
}
|
||||
|
||||
|
@ -434,6 +439,7 @@ export class FeedModel {
|
|||
}
|
||||
|
||||
private _prependAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
|
||||
this.pollCursor = res.data.feed[0]?.uri
|
||||
let counter = this.feed.length
|
||||
const toPrepend = []
|
||||
for (const item of res.data.feed) {
|
||||
|
@ -493,8 +499,7 @@ function preprocessFeed(
|
|||
for (let i = feed.length - 1; i >= 0; i--) {
|
||||
const item = feed[i] as FeedItemWithThreadMeta
|
||||
|
||||
// dont dedup the first item so that polling works properly
|
||||
if (dedup && i !== 0) {
|
||||
if (dedup) {
|
||||
if (reorg.find(item2 => item2.uri === item.uri)) {
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import {APP_BSKY_GRAPH} from '../../third-party/api'
|
|||
import {cleanError} from '../../lib/strings'
|
||||
|
||||
const UNGROUPABLE_REASONS = ['trend', 'assertion']
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
const MS_60MIN = 1e3 * 60 * 60
|
||||
|
||||
export interface GroupedNotification extends ListNotifications.Notification {
|
||||
|
@ -242,9 +242,10 @@ export class NotificationsViewModel {
|
|||
private async _initialLoad(isRefreshing = false) {
|
||||
this._xLoading(isRefreshing)
|
||||
try {
|
||||
const res = await this.rootStore.api.app.bsky.notification.list(
|
||||
this.params,
|
||||
)
|
||||
const params = Object.assign({}, this.params, {
|
||||
limit: PAGE_SIZE,
|
||||
})
|
||||
const res = await this.rootStore.api.app.bsky.notification.list(params)
|
||||
this._replaceAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
|
@ -259,6 +260,7 @@ export class NotificationsViewModel {
|
|||
this._xLoading()
|
||||
try {
|
||||
const params = Object.assign({}, this.params, {
|
||||
limit: PAGE_SIZE,
|
||||
before: this.loadMoreCursor,
|
||||
})
|
||||
const res = await this.rootStore.api.app.bsky.notification.list(params)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, {useEffect, useMemo, useState} from 'react'
|
||||
import React, {useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
|
@ -17,9 +17,10 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|||
import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view'
|
||||
import {UserLocalPhotosModel} from '../../../state/models/user-local-photos'
|
||||
import {Autocomplete} from './Autocomplete'
|
||||
import Toast from '../util/Toast'
|
||||
import * as Toast from '../util/Toast'
|
||||
import ProgressCircle from '../util/ProgressCircle'
|
||||
import {TextLink} from '../util/Link'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import {useStores} from '../../../state'
|
||||
import * as apilib from '../../../state/lib/api'
|
||||
import {ComposerOpts} from '../../../state/models/shell-ui'
|
||||
|
@ -28,7 +29,6 @@ import {detectLinkables} from '../../../lib/strings'
|
|||
import {openPicker, openCamera} from 'react-native-image-crop-picker'
|
||||
|
||||
const MAX_TEXT_LENGTH = 256
|
||||
const WARNING_TEXT_LENGTH = 200
|
||||
const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH
|
||||
|
||||
export const ComposePost = observer(function ComposePost({
|
||||
|
@ -41,6 +41,7 @@ export const ComposePost = observer(function ComposePost({
|
|||
onClose: () => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
const textInput = useRef<TextInput>(null)
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [text, setText] = useState('')
|
||||
|
@ -57,6 +58,22 @@ export const ComposePost = observer(function ComposePost({
|
|||
useEffect(() => {
|
||||
autocompleteView.setup()
|
||||
})
|
||||
useEffect(() => {
|
||||
// HACK
|
||||
// wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
|
||||
// -prf
|
||||
let to: NodeJS.Timeout | undefined
|
||||
if (textInput.current) {
|
||||
to = setTimeout(() => {
|
||||
textInput.current?.focus()
|
||||
}, 250)
|
||||
}
|
||||
return () => {
|
||||
if (to) {
|
||||
clearTimeout(to)
|
||||
}
|
||||
}
|
||||
}, [textInput.current])
|
||||
|
||||
useEffect(() => {
|
||||
localPhotos.setup()
|
||||
|
@ -90,7 +107,10 @@ export const ComposePost = observer(function ComposePost({
|
|||
}
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
await apilib.post(store, text, replyTo, autocompleteView.knownHandles)
|
||||
const replyRef = replyTo
|
||||
? {uri: replyTo.uri, cid: replyTo.cid}
|
||||
: undefined
|
||||
await apilib.post(store, text, replyRef, autocompleteView.knownHandles)
|
||||
} catch (e: any) {
|
||||
console.error(`Failed to create post: ${e.toString()}`)
|
||||
setError(
|
||||
|
@ -101,13 +121,7 @@ export const ComposePost = observer(function ComposePost({
|
|||
}
|
||||
onPost?.()
|
||||
onClose()
|
||||
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`, {
|
||||
duration: Toast.durations.LONG,
|
||||
position: Toast.positions.TOP,
|
||||
shadow: true,
|
||||
animation: true,
|
||||
hideOnPress: true,
|
||||
})
|
||||
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)
|
||||
}
|
||||
const onSelectAutocompleteItem = (item: string) => {
|
||||
setText(replaceTextAutocompletePrefix(text, item))
|
||||
|
@ -115,12 +129,7 @@ export const ComposePost = observer(function ComposePost({
|
|||
}
|
||||
|
||||
const canPost = text.length <= MAX_TEXT_LENGTH
|
||||
const progressColor =
|
||||
text.length > DANGER_TEXT_LENGTH
|
||||
? '#e60000'
|
||||
: text.length > WARNING_TEXT_LENGTH
|
||||
? '#f7c600'
|
||||
: undefined
|
||||
const progressColor = text.length > DANGER_TEXT_LENGTH ? '#e60000' : undefined
|
||||
|
||||
const textDecorated = useMemo(() => {
|
||||
let i = 0
|
||||
|
@ -142,7 +151,7 @@ export const ComposePost = observer(function ComposePost({
|
|||
<SafeAreaView style={s.flex1}>
|
||||
<View style={styles.topbar}>
|
||||
<TouchableOpacity onPress={onPressCancel}>
|
||||
<Text style={[s.blue3, s.f16]}>Cancel</Text>
|
||||
<Text style={[s.blue3, s.f18]}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{isProcessing ? (
|
||||
|
@ -156,7 +165,9 @@ export const ComposePost = observer(function ComposePost({
|
|||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={styles.postBtn}>
|
||||
<Text style={[s.white, s.f16, s.bold]}>Post</Text>
|
||||
<Text style={[s.white, s.f16, s.bold]}>
|
||||
{replyTo ? 'Reply' : 'Post'}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
|
@ -178,39 +189,46 @@ export const ComposePost = observer(function ComposePost({
|
|||
</View>
|
||||
)}
|
||||
{replyTo ? (
|
||||
<View>
|
||||
<Text style={s.gray4}>
|
||||
Replying to{' '}
|
||||
<View style={styles.replyToLayout}>
|
||||
<UserAvatar
|
||||
handle={replyTo.author.handle}
|
||||
displayName={replyTo.author.displayName}
|
||||
size={50}
|
||||
/>
|
||||
<View style={styles.replyToPost}>
|
||||
<TextLink
|
||||
href={`/profile/${replyTo.author.handle}`}
|
||||
text={'@' + replyTo.author.handle}
|
||||
style={[s.bold, s.gray5]}
|
||||
text={replyTo.author.displayName || replyTo.author.handle}
|
||||
style={[s.f16, s.bold]}
|
||||
/>
|
||||
</Text>
|
||||
<View style={styles.replyToPost}>
|
||||
<Text style={s.gray5}>{replyTo.text}</Text>
|
||||
<Text style={[s.f16, s['lh16-1.3']]} numberOfLines={6}>
|
||||
{replyTo.text}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : undefined}
|
||||
<TextInput
|
||||
multiline
|
||||
scrollEnabled
|
||||
onChangeText={(text: string) => onChangeText(text)}
|
||||
placeholder={
|
||||
replyTo
|
||||
? 'Write your reply'
|
||||
: photoUris.length === 0
|
||||
? "What's up?"
|
||||
: 'Add a comment...'
|
||||
}
|
||||
style={styles.textInput}>
|
||||
{textDecorated}
|
||||
</TextInput>
|
||||
<View style={styles.textInputLayout}>
|
||||
<UserAvatar
|
||||
handle={store.me.handle || ''}
|
||||
displayName={store.me.displayName}
|
||||
size={50}
|
||||
/>
|
||||
<TextInput
|
||||
ref={textInput}
|
||||
multiline
|
||||
scrollEnabled
|
||||
onChangeText={(text: string) => onChangeText(text)}
|
||||
placeholder={replyTo ? 'Write your reply' : "What's up?"}
|
||||
style={styles.textInput}>
|
||||
{textDecorated}
|
||||
</TextInput>
|
||||
</View>
|
||||
{photoUris.length !== 0 && (
|
||||
<View style={styles.selectedImageContainer}>
|
||||
{photoUris.length !== 0 &&
|
||||
photoUris.map(item => (
|
||||
photoUris.map((item, index) => (
|
||||
<View
|
||||
key={`selected-image-${index}`}
|
||||
style={[
|
||||
styles.selectedImage,
|
||||
photoUris.length === 1
|
||||
|
@ -264,8 +282,9 @@ export const ComposePost = observer(function ComposePost({
|
|||
style={{color: colors.blue3}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
{localPhotos.photos.map(item => (
|
||||
{localPhotos.photos.map((item, index) => (
|
||||
<TouchableOpacity
|
||||
key={`local-image-${index}`}
|
||||
style={styles.photoButton}
|
||||
onPress={() => {
|
||||
setPhotoUris([item.node.image.uri, ...photoUris])
|
||||
|
@ -343,9 +362,9 @@ const styles = StyleSheet.create({
|
|||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingTop: 10,
|
||||
paddingBottom: 5,
|
||||
paddingBottom: 10,
|
||||
paddingHorizontal: 5,
|
||||
height: 50,
|
||||
height: 55,
|
||||
},
|
||||
postBtn: {
|
||||
borderRadius: 20,
|
||||
|
@ -371,19 +390,30 @@ const styles = StyleSheet.create({
|
|||
justifyContent: 'center',
|
||||
marginRight: 5,
|
||||
},
|
||||
textInputLayout: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.gray2,
|
||||
paddingTop: 16,
|
||||
},
|
||||
textInput: {
|
||||
flex: 1,
|
||||
padding: 5,
|
||||
fontSize: 21,
|
||||
fontSize: 18,
|
||||
marginLeft: 8,
|
||||
},
|
||||
replyToLayout: {
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.gray2,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
replyToPost: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 6,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.gray2,
|
||||
borderRadius: 6,
|
||||
marginTop: 5,
|
||||
marginBottom: 10,
|
||||
flex: 1,
|
||||
paddingLeft: 13,
|
||||
paddingRight: 8,
|
||||
},
|
||||
contentCenter: {alignItems: 'center'},
|
||||
selectedImageContainer: {
|
||||
|
|
|
@ -1,29 +1,42 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {colors} from '../../lib/styles'
|
||||
import {useStores} from '../../../state'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
|
||||
export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
|
||||
export function ComposePrompt({
|
||||
noAvi = false,
|
||||
text = "What's up?",
|
||||
btn = 'Post',
|
||||
onPressCompose,
|
||||
}: {
|
||||
noAvi?: boolean
|
||||
text?: string
|
||||
btn?: string
|
||||
onPressCompose: () => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
const onPressAvatar = () => {
|
||||
store.nav.navigate(`/profile/${store.me.handle}`)
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity style={styles.container} onPress={onPressCompose}>
|
||||
<TouchableOpacity style={styles.avatar} onPress={onPressAvatar}>
|
||||
<UserAvatar
|
||||
size={50}
|
||||
handle={store.me.handle || ''}
|
||||
displayName={store.me.displayName}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.container, noAvi ? styles.noAviContainer : undefined]}
|
||||
onPress={onPressCompose}>
|
||||
{!noAvi ? (
|
||||
<TouchableOpacity style={styles.avatar} onPress={onPressAvatar}>
|
||||
<UserAvatar
|
||||
size={50}
|
||||
handle={store.me.handle || ''}
|
||||
displayName={store.me.displayName}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
) : undefined}
|
||||
<View style={styles.textContainer}>
|
||||
<Text style={styles.text}>What's up?</Text>
|
||||
<Text style={styles.text}>{text}</Text>
|
||||
</View>
|
||||
<View style={styles.btn}>
|
||||
<Text style={styles.btnText}>Post</Text>
|
||||
<Text style={styles.btnText}>{btn}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
|
@ -40,6 +53,9 @@ const styles = StyleSheet.create({
|
|||
alignItems: 'center',
|
||||
backgroundColor: colors.white,
|
||||
},
|
||||
noAviContainer: {
|
||||
paddingVertical: 14,
|
||||
},
|
||||
avatar: {
|
||||
width: 50,
|
||||
},
|
||||
|
|
|
@ -14,7 +14,7 @@ import _omit from 'lodash.omit'
|
|||
import {ErrorScreen} from '../util/ErrorScreen'
|
||||
import {Link} from '../util/Link'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import Toast from '../util/Toast'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {useStores} from '../../../state'
|
||||
import * as apilib from '../../../state/lib/api'
|
||||
import {
|
||||
|
@ -63,10 +63,7 @@ export const SuggestedFollows = observer(
|
|||
setFollows({[item.did]: res.uri, ...follows})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
Toast.show('An issue occurred, please try again.', {
|
||||
duration: Toast.durations.LONG,
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
Toast.show('An issue occurred, please try again.')
|
||||
}
|
||||
}
|
||||
const onPressUnfollow = async (item: SuggestedActor) => {
|
||||
|
@ -75,10 +72,7 @@ export const SuggestedFollows = observer(
|
|||
setFollows(_omit(follows, [item.did]))
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
Toast.show('An issue occurred, please try again.', {
|
||||
duration: Toast.durations.LONG,
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
Toast.show('An issue occurred, please try again.')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, {useState} from 'react'
|
||||
import Toast from '../util/Toast'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
|
@ -71,9 +71,7 @@ export function Component({}: {}) {
|
|||
},
|
||||
)
|
||||
.catch(e => console.error(e)) // an error here is not critical
|
||||
Toast.show('Scene created', {
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
Toast.show('Scene created')
|
||||
store.shell.closeModal()
|
||||
store.nav.navigate(`/profile/${fullHandle}`)
|
||||
} catch (e: any) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, {useState} from 'react'
|
||||
import Toast from '../util/Toast'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet'
|
||||
|
@ -52,9 +52,7 @@ export function Component({
|
|||
}
|
||||
},
|
||||
)
|
||||
Toast.show('Profile updated', {
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
Toast.show('Profile updated')
|
||||
onUpdate?.()
|
||||
store.shell.closeModal()
|
||||
} catch (e: any) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, {useState, useEffect, useMemo} from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import Toast from '../util/Toast'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
|
@ -83,10 +83,7 @@ export const Component = observer(function Component({
|
|||
follow.declaration.cid,
|
||||
)
|
||||
setCreatedInvites({[follow.did]: assertionUri, ...createdInvites})
|
||||
Toast.show('Invite sent', {
|
||||
duration: Toast.durations.LONG,
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
Toast.show('Invite sent')
|
||||
} catch (e) {
|
||||
setError('There was an issue with the invite. Please try again.')
|
||||
console.error(e)
|
||||
|
@ -119,10 +116,7 @@ export const Component = observer(function Component({
|
|||
[assertion.uri]: true,
|
||||
...deletedPendingInvites,
|
||||
})
|
||||
Toast.show('Invite removed', {
|
||||
duration: Toast.durations.LONG,
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
Toast.show('Invite removed')
|
||||
} catch (e) {
|
||||
setError('There was an issue with the invite. Please try again.')
|
||||
console.error(e)
|
||||
|
|
|
@ -7,7 +7,7 @@ import {NotificationsViewItemModel} from '../../../state/models/notifications-vi
|
|||
import {ConfirmModel} from '../../../state/models/shell-ui'
|
||||
import {useStores} from '../../../state'
|
||||
import {ProfileCard} from '../profile/ProfileCard'
|
||||
import Toast from '../util/Toast'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {s, colors, gradients} from '../../lib/styles'
|
||||
|
||||
export function InviteAccepter({item}: {item: NotificationsViewItemModel}) {
|
||||
|
@ -46,10 +46,7 @@ export function InviteAccepter({item}: {item: NotificationsViewItemModel}) {
|
|||
},
|
||||
})
|
||||
store.me.refreshMemberships()
|
||||
Toast.show('Invite accepted', {
|
||||
duration: Toast.durations.LONG,
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
Toast.show('Invite accepted')
|
||||
setConfirmationUri(uri)
|
||||
}
|
||||
return (
|
||||
|
|
|
@ -8,7 +8,7 @@ import {PostThreadViewPostModel} from '../../../state/models/post-thread-view'
|
|||
import {Link} from '../util/Link'
|
||||
import {RichText} from '../util/RichText'
|
||||
import {PostDropdownBtn} from '../util/DropdownBtn'
|
||||
import Toast from '../util/Toast'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import {s, colors} from '../../lib/styles'
|
||||
import {ago, pluralize} from '../../../lib/strings'
|
||||
|
@ -16,6 +16,7 @@ import {useStores} from '../../../state'
|
|||
import {PostMeta} from '../util/PostMeta'
|
||||
import {PostEmbeds} from '../util/PostEmbeds'
|
||||
import {PostCtrls} from '../util/PostCtrls'
|
||||
import {ComposePrompt} from '../composer/Prompt'
|
||||
|
||||
const PARENT_REPLY_LINE_LENGTH = 8
|
||||
const REPLYING_TO_LINE_LENGTH = 6
|
||||
|
@ -78,131 +79,133 @@ export const PostThreadItem = observer(function PostThreadItem({
|
|||
item.delete().then(
|
||||
() => {
|
||||
setDeleted(true)
|
||||
Toast.show('Post deleted', {
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
Toast.show('Post deleted')
|
||||
},
|
||||
e => {
|
||||
console.error(e)
|
||||
Toast.show('Failed to delete post, please try again', {
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
Toast.show('Failed to delete post, please try again')
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (item._isHighlightedPost) {
|
||||
return (
|
||||
<View style={styles.outer}>
|
||||
<View style={styles.layout}>
|
||||
<View style={styles.layoutAvi}>
|
||||
<Link href={authorHref} title={authorTitle}>
|
||||
<UserAvatar
|
||||
size={50}
|
||||
displayName={item.author.displayName}
|
||||
handle={item.author.handle}
|
||||
/>
|
||||
</Link>
|
||||
</View>
|
||||
<View style={styles.layoutContent}>
|
||||
<View style={[styles.meta, {paddingTop: 5, paddingBottom: 0}]}>
|
||||
<Link
|
||||
style={styles.metaItem}
|
||||
href={authorHref}
|
||||
title={authorTitle}>
|
||||
<Text style={[s.f16, s.bold]} numberOfLines={1}>
|
||||
{item.author.displayName || item.author.handle}
|
||||
</Text>
|
||||
</Link>
|
||||
<Text style={[styles.metaItem, s.f15, s.gray5]}>
|
||||
· {ago(item.indexedAt)}
|
||||
</Text>
|
||||
<View style={s.flex1} />
|
||||
<PostDropdownBtn
|
||||
style={styles.metaItem}
|
||||
itemHref={itemHref}
|
||||
itemTitle={itemTitle}
|
||||
isAuthor={item.author.did === store.me.did}
|
||||
onDeletePost={onDeletePost}>
|
||||
<FontAwesomeIcon
|
||||
icon="ellipsis-h"
|
||||
size={14}
|
||||
style={[s.mt2, s.mr5]}
|
||||
<>
|
||||
<View style={styles.outer}>
|
||||
<View style={styles.layout}>
|
||||
<View style={styles.layoutAvi}>
|
||||
<Link href={authorHref} title={authorTitle}>
|
||||
<UserAvatar
|
||||
size={50}
|
||||
displayName={item.author.displayName}
|
||||
handle={item.author.handle}
|
||||
/>
|
||||
</PostDropdownBtn>
|
||||
</View>
|
||||
<View style={styles.meta}>
|
||||
<Link
|
||||
style={styles.metaItem}
|
||||
href={authorHref}
|
||||
title={authorTitle}>
|
||||
<Text style={[s.f15, s.gray5]} numberOfLines={1}>
|
||||
@{item.author.handle}
|
||||
</Text>
|
||||
</Link>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[s.pl10, s.pr10, s.pb10]}>
|
||||
<View
|
||||
style={[styles.postTextContainer, styles.postTextLargeContainer]}>
|
||||
<RichText
|
||||
text={record.text}
|
||||
entities={record.entities}
|
||||
style={[styles.postText, styles.postTextLarge]}
|
||||
/>
|
||||
</View>
|
||||
<PostEmbeds entities={record.entities} />
|
||||
{item._isHighlightedPost && hasEngagement ? (
|
||||
<View style={styles.expandedInfo}>
|
||||
{item.repostCount ? (
|
||||
<View style={styles.layoutContent}>
|
||||
<View style={[styles.meta, {paddingTop: 5, paddingBottom: 0}]}>
|
||||
<Link
|
||||
style={styles.expandedInfoItem}
|
||||
href={repostsHref}
|
||||
title={repostsTitle}>
|
||||
<Text style={[s.gray5, s.semiBold, s.f18]}>
|
||||
<Text style={[s.bold, s.black, s.f18]}>
|
||||
{item.repostCount}
|
||||
</Text>{' '}
|
||||
{pluralize(item.repostCount, 'repost')}
|
||||
style={styles.metaItem}
|
||||
href={authorHref}
|
||||
title={authorTitle}>
|
||||
<Text style={[s.f16, s.bold]} numberOfLines={1}>
|
||||
{item.author.displayName || item.author.handle}
|
||||
</Text>
|
||||
</Link>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{item.upvoteCount ? (
|
||||
<Text style={[styles.metaItem, s.f15, s.gray5]}>
|
||||
· {ago(item.indexedAt)}
|
||||
</Text>
|
||||
<View style={s.flex1} />
|
||||
<PostDropdownBtn
|
||||
style={styles.metaItem}
|
||||
itemHref={itemHref}
|
||||
itemTitle={itemTitle}
|
||||
isAuthor={item.author.did === store.me.did}
|
||||
onDeletePost={onDeletePost}>
|
||||
<FontAwesomeIcon
|
||||
icon="ellipsis-h"
|
||||
size={14}
|
||||
style={[s.mt2, s.mr5]}
|
||||
/>
|
||||
</PostDropdownBtn>
|
||||
</View>
|
||||
<View style={styles.meta}>
|
||||
<Link
|
||||
style={styles.expandedInfoItem}
|
||||
href={upvotesHref}
|
||||
title={upvotesTitle}>
|
||||
<Text style={[s.gray5, s.semiBold, s.f18]}>
|
||||
<Text style={[s.bold, s.black, s.f18]}>
|
||||
{item.upvoteCount}
|
||||
</Text>{' '}
|
||||
{pluralize(item.upvoteCount, 'upvote')}
|
||||
style={styles.metaItem}
|
||||
href={authorHref}
|
||||
title={authorTitle}>
|
||||
<Text style={[s.f15, s.gray5]} numberOfLines={1}>
|
||||
@{item.author.handle}
|
||||
</Text>
|
||||
</Link>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[s.pl10, s.pr10, s.pb10]}>
|
||||
<View
|
||||
style={[styles.postTextContainer, styles.postTextLargeContainer]}>
|
||||
<RichText
|
||||
text={record.text}
|
||||
entities={record.entities}
|
||||
style={[styles.postText, styles.postTextLarge]}
|
||||
/>
|
||||
</View>
|
||||
<PostEmbeds entities={record.entities} style={s.mb10} />
|
||||
{item._isHighlightedPost && hasEngagement ? (
|
||||
<View style={styles.expandedInfo}>
|
||||
{item.repostCount ? (
|
||||
<Link
|
||||
style={styles.expandedInfoItem}
|
||||
href={repostsHref}
|
||||
title={repostsTitle}>
|
||||
<Text style={[s.gray5, s.semiBold, s.f17]}>
|
||||
<Text style={[s.bold, s.black, s.f17]}>
|
||||
{item.repostCount}
|
||||
</Text>{' '}
|
||||
{pluralize(item.repostCount, 'repost')}
|
||||
</Text>
|
||||
</Link>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{item.upvoteCount ? (
|
||||
<Link
|
||||
style={styles.expandedInfoItem}
|
||||
href={upvotesHref}
|
||||
title={upvotesTitle}>
|
||||
<Text style={[s.gray5, s.semiBold, s.f17]}>
|
||||
<Text style={[s.bold, s.black, s.f17]}>
|
||||
{item.upvoteCount}
|
||||
</Text>{' '}
|
||||
{pluralize(item.upvoteCount, 'upvote')}
|
||||
</Text>
|
||||
</Link>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<View style={[s.pl10, s.pb5]}>
|
||||
<PostCtrls
|
||||
big
|
||||
isReposted={!!item.myState.repost}
|
||||
isUpvoted={!!item.myState.upvote}
|
||||
onPressReply={onPressReply}
|
||||
onPressToggleRepost={onPressToggleRepost}
|
||||
onPressToggleUpvote={onPressToggleUpvote}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<View style={[s.pl10]}>
|
||||
<PostCtrls
|
||||
replyCount={item.replyCount}
|
||||
repostCount={item.repostCount}
|
||||
upvoteCount={item.upvoteCount}
|
||||
isReposted={!!item.myState.repost}
|
||||
isUpvoted={!!item.myState.upvote}
|
||||
onPressReply={onPressReply}
|
||||
onPressToggleRepost={onPressToggleRepost}
|
||||
onPressToggleUpvote={onPressToggleUpvote}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<ComposePrompt
|
||||
noAvi
|
||||
text="Write your reply"
|
||||
btn="Reply"
|
||||
onPressCompose={onPressReply}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
|
@ -345,8 +348,8 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
postText: {
|
||||
fontFamily: 'Helvetica Neue',
|
||||
fontSize: 17,
|
||||
lineHeight: 22.1, // 1.3 of 17px
|
||||
fontSize: 16,
|
||||
lineHeight: 20.8, // 1.3 of 16px
|
||||
},
|
||||
postTextContainer: {
|
||||
flexDirection: 'row',
|
||||
|
@ -371,7 +374,7 @@ const styles = StyleSheet.create({
|
|||
borderTopWidth: 1,
|
||||
borderBottomWidth: 1,
|
||||
marginTop: 5,
|
||||
marginBottom: 10,
|
||||
marginBottom: 15,
|
||||
},
|
||||
expandedInfoItem: {
|
||||
marginRight: 10,
|
||||
|
|
|
@ -10,7 +10,7 @@ import {UserInfoText} from '../util/UserInfoText'
|
|||
import {PostMeta} from '../util/PostMeta'
|
||||
import {PostCtrls} from '../util/PostCtrls'
|
||||
import {RichText} from '../util/RichText'
|
||||
import Toast from '../util/Toast'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import {useStores} from '../../../state'
|
||||
import {s, colors} from '../../lib/styles'
|
||||
|
@ -99,15 +99,11 @@ export const Post = observer(function Post({uri}: {uri: string}) {
|
|||
item.delete().then(
|
||||
() => {
|
||||
setDeleted(true)
|
||||
Toast.show('Post deleted', {
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
Toast.show('Post deleted')
|
||||
},
|
||||
e => {
|
||||
console.error(e)
|
||||
Toast.show('Failed to delete post, please try again', {
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
Toast.show('Failed to delete post, please try again')
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -196,7 +192,7 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
postText: {
|
||||
fontFamily: 'Helvetica Neue',
|
||||
fontSize: 17,
|
||||
lineHeight: 22.1, // 1.3 of 17px
|
||||
fontSize: 16,
|
||||
lineHeight: 20.8, // 1.3 of 16px
|
||||
},
|
||||
})
|
||||
|
|
|
@ -11,7 +11,7 @@ import {PostMeta} from '../util/PostMeta'
|
|||
import {PostCtrls} from '../util/PostCtrls'
|
||||
import {PostEmbeds} from '../util/PostEmbeds'
|
||||
import {RichText} from '../util/RichText'
|
||||
import Toast from '../util/Toast'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import {s, colors} from '../../lib/styles'
|
||||
import {useStores} from '../../../state'
|
||||
|
@ -70,15 +70,11 @@ export const FeedItem = observer(function FeedItem({
|
|||
item.delete().then(
|
||||
() => {
|
||||
setDeleted(true)
|
||||
Toast.show('Post deleted', {
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
Toast.show('Post deleted')
|
||||
},
|
||||
e => {
|
||||
console.error(e)
|
||||
Toast.show('Failed to delete post, please try again', {
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
Toast.show('Failed to delete post, please try again')
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -254,7 +250,7 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
postText: {
|
||||
fontFamily: 'Helvetica Neue',
|
||||
fontSize: 17,
|
||||
lineHeight: 22.1, // 1.3 of 17px
|
||||
fontSize: 16,
|
||||
lineHeight: 20.8, // 1.3 of 16px
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
import React, {useMemo} from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {AtUri} from '../../../third-party/uri'
|
||||
|
@ -20,9 +14,8 @@ import {
|
|||
import {pluralize} from '../../../lib/strings'
|
||||
import {s, colors} from '../../lib/styles'
|
||||
import {getGradient} from '../../lib/asset-gen'
|
||||
import {MagnifyingGlassIcon} from '../../lib/icons'
|
||||
import {DropdownBtn, DropdownItem} from '../util/DropdownBtn'
|
||||
import Toast from '../util/Toast'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||
import {RichText} from '../util/RichText'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
|
@ -55,10 +48,6 @@ export const ProfileHeader = observer(function ProfileHeader({
|
|||
`${view.myState.follow ? 'Following' : 'No longer following'} ${
|
||||
view.displayName || view.handle
|
||||
}`,
|
||||
{
|
||||
duration: Toast.durations.LONG,
|
||||
position: Toast.positions.TOP,
|
||||
},
|
||||
)
|
||||
},
|
||||
err => console.error('Failed to toggle follow', err),
|
||||
|
@ -94,10 +83,7 @@ export const ProfileHeader = observer(function ProfileHeader({
|
|||
did: store.me.did || '',
|
||||
rkey: new AtUri(view.myState.member).rkey,
|
||||
})
|
||||
Toast.show(`Scene left`, {
|
||||
duration: Toast.durations.LONG,
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
Toast.show(`Scene left`)
|
||||
}
|
||||
onRefreshAll()
|
||||
}
|
||||
|
@ -108,18 +94,6 @@ export const ProfileHeader = observer(function ProfileHeader({
|
|||
return (
|
||||
<View style={styles.outer}>
|
||||
<LoadingPlaceholder width="100%" height={120} />
|
||||
{store.nav.tab.canGoBack ? (
|
||||
<TouchableOpacity style={styles.backButton} onPress={onPressBack}>
|
||||
<FontAwesomeIcon
|
||||
size={18}
|
||||
icon="angle-left"
|
||||
style={styles.backIcon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
) : undefined}
|
||||
<TouchableOpacity style={styles.searchBtn} onPress={onPressSearch}>
|
||||
<MagnifyingGlassIcon size={19} style={styles.searchIcon} />
|
||||
</TouchableOpacity>
|
||||
<View style={styles.avi}>
|
||||
<LoadingPlaceholder
|
||||
width={80}
|
||||
|
@ -179,18 +153,6 @@ export const ProfileHeader = observer(function ProfileHeader({
|
|||
return (
|
||||
<View style={styles.outer}>
|
||||
<UserBanner handle={view.handle} />
|
||||
{store.nav.tab.canGoBack ? (
|
||||
<TouchableOpacity style={styles.backButton} onPress={onPressBack}>
|
||||
<FontAwesomeIcon
|
||||
size={18}
|
||||
icon="angle-left"
|
||||
style={styles.backIcon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
) : undefined}
|
||||
<TouchableOpacity style={styles.searchBtn} onPress={onPressSearch}>
|
||||
<MagnifyingGlassIcon size={19} style={styles.searchIcon} />
|
||||
</TouchableOpacity>
|
||||
<View style={styles.avi}>
|
||||
<UserAvatar
|
||||
size={80}
|
||||
|
@ -353,30 +315,6 @@ const styles = StyleSheet.create({
|
|||
width: '100%',
|
||||
height: 120,
|
||||
},
|
||||
backButton: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: 12,
|
||||
backgroundColor: '#ffff',
|
||||
padding: 6,
|
||||
borderRadius: 30,
|
||||
},
|
||||
backIcon: {
|
||||
width: 14,
|
||||
height: 14,
|
||||
color: colors.black,
|
||||
},
|
||||
searchBtn: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
right: 12,
|
||||
backgroundColor: '#ffff',
|
||||
padding: 5,
|
||||
borderRadius: 30,
|
||||
},
|
||||
searchIcon: {
|
||||
color: colors.black,
|
||||
},
|
||||
avi: {
|
||||
position: 'absolute',
|
||||
top: 80,
|
||||
|
|
|
@ -12,9 +12,10 @@ import {UpIcon, UpIconSolid} from '../../lib/icons'
|
|||
import {s, colors} from '../../lib/styles'
|
||||
|
||||
interface PostCtrlsOpts {
|
||||
replyCount: number
|
||||
repostCount: number
|
||||
upvoteCount: number
|
||||
big?: boolean
|
||||
replyCount?: number
|
||||
repostCount?: number
|
||||
upvoteCount?: number
|
||||
isReposted: boolean
|
||||
isUpvoted: boolean
|
||||
onPressReply: () => void
|
||||
|
@ -30,17 +31,17 @@ export function PostCtrls(opts: PostCtrlsOpts) {
|
|||
const interp2 = useSharedValue<number>(0)
|
||||
|
||||
const anim1Style = useAnimatedStyle(() => ({
|
||||
transform: [{scale: interpolate(interp1.value, [0, 1.0], [1.0, 3.0])}],
|
||||
transform: [{scale: interpolate(interp1.value, [0, 1.0], [1.0, 4.0])}],
|
||||
opacity: interpolate(interp1.value, [0, 1.0], [1.0, 0.0]),
|
||||
}))
|
||||
const anim2Style = useAnimatedStyle(() => ({
|
||||
transform: [{scale: interpolate(interp2.value, [0, 1.0], [1.0, 3.0])}],
|
||||
transform: [{scale: interpolate(interp2.value, [0, 1.0], [1.0, 4.0])}],
|
||||
opacity: interpolate(interp2.value, [0, 1.0], [1.0, 0.0]),
|
||||
}))
|
||||
|
||||
const onPressToggleRepostWrapper = () => {
|
||||
if (!opts.isReposted) {
|
||||
interp1.value = withTiming(1, {duration: 300}, () => {
|
||||
interp1.value = withTiming(1, {duration: 400}, () => {
|
||||
interp1.value = withDelay(100, withTiming(0, {duration: 20}))
|
||||
})
|
||||
}
|
||||
|
@ -48,7 +49,7 @@ export function PostCtrls(opts: PostCtrlsOpts) {
|
|||
}
|
||||
const onPressToggleUpvoteWrapper = () => {
|
||||
if (!opts.isUpvoted) {
|
||||
interp2.value = withTiming(1, {duration: 300}, () => {
|
||||
interp2.value = withTiming(1, {duration: 400}, () => {
|
||||
interp2.value = withDelay(100, withTiming(0, {duration: 20}))
|
||||
})
|
||||
}
|
||||
|
@ -62,9 +63,11 @@ export function PostCtrls(opts: PostCtrlsOpts) {
|
|||
<FontAwesomeIcon
|
||||
style={styles.ctrlIcon}
|
||||
icon={['far', 'comment']}
|
||||
size={14}
|
||||
size={opts.big ? 20 : 14}
|
||||
/>
|
||||
<Text style={[sRedgray, s.ml5, s.f16]}>{opts.replyCount}</Text>
|
||||
{typeof opts.replyCount !== 'undefined' ? (
|
||||
<Text style={[sRedgray, s.ml5, s.f16]}>{opts.replyCount}</Text>
|
||||
) : undefined}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={s.flex1}>
|
||||
|
@ -77,17 +80,19 @@ export function PostCtrls(opts: PostCtrlsOpts) {
|
|||
opts.isReposted ? styles.ctrlIconReposted : styles.ctrlIcon
|
||||
}
|
||||
icon="retweet"
|
||||
size={18}
|
||||
size={opts.big ? 22 : 18}
|
||||
/>
|
||||
</Animated.View>
|
||||
<Text
|
||||
style={
|
||||
opts.isReposted
|
||||
? [s.bold, s.green3, s.f16, s.ml5]
|
||||
: [sRedgray, s.f16, s.ml5]
|
||||
}>
|
||||
{opts.repostCount}
|
||||
</Text>
|
||||
{typeof opts.repostCount !== 'undefined' ? (
|
||||
<Text
|
||||
style={
|
||||
opts.isReposted
|
||||
? [s.bold, s.green3, s.f16, s.ml5]
|
||||
: [sRedgray, s.f16, s.ml5]
|
||||
}>
|
||||
{opts.repostCount}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={s.flex1}>
|
||||
|
@ -96,19 +101,28 @@ export function PostCtrls(opts: PostCtrlsOpts) {
|
|||
onPress={onPressToggleUpvoteWrapper}>
|
||||
<Animated.View style={anim2Style}>
|
||||
{opts.isUpvoted ? (
|
||||
<UpIconSolid style={[styles.ctrlIconUpvoted]} size={18} />
|
||||
<UpIconSolid
|
||||
style={[styles.ctrlIconUpvoted]}
|
||||
size={opts.big ? 22 : 18}
|
||||
/>
|
||||
) : (
|
||||
<UpIcon style={[styles.ctrlIcon]} size={18} strokeWidth={1.5} />
|
||||
<UpIcon
|
||||
style={[styles.ctrlIcon]}
|
||||
size={opts.big ? 22 : 18}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
)}
|
||||
</Animated.View>
|
||||
<Text
|
||||
style={
|
||||
opts.isUpvoted
|
||||
? [s.bold, s.red3, s.f16, s.ml5]
|
||||
: [sRedgray, s.f16, s.ml5]
|
||||
}>
|
||||
{opts.upvoteCount}
|
||||
</Text>
|
||||
{typeof opts.upvoteCount !== 'undefined' ? (
|
||||
<Text
|
||||
style={
|
||||
opts.isUpvoted
|
||||
? [s.bold, s.red3, s.f16, s.ml5]
|
||||
: [sRedgray, s.f16, s.ml5]
|
||||
}>
|
||||
{opts.upvoteCount}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={s.flex1}></View>
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
import Toast from 'react-native-root-toast'
|
||||
export default Toast
|
|
@ -1,62 +1,11 @@
|
|||
/*
|
||||
* Note: the dataSet properties are used to leverage custom CSS in public/index.html
|
||||
*/
|
||||
import Toast from 'react-native-root-toast'
|
||||
|
||||
import React, {useState, useEffect} from 'react'
|
||||
// @ts-ignore no declarations available -prf
|
||||
import {Text, View} from 'react-native-web'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
|
||||
interface ActiveToast {
|
||||
text: string
|
||||
}
|
||||
type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void
|
||||
|
||||
// globals
|
||||
// =
|
||||
let globalSetActiveToast: GlobalSetActiveToast | undefined
|
||||
let toastTimeout: NodeJS.Timeout | undefined
|
||||
|
||||
// components
|
||||
// =
|
||||
type ToastContainerProps = {}
|
||||
const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
|
||||
const [activeToast, setActiveToast] = useState<ActiveToast | undefined>()
|
||||
useEffect(() => {
|
||||
globalSetActiveToast = (t: ActiveToast | undefined) => {
|
||||
setActiveToast(t)
|
||||
}
|
||||
export function show(message: string) {
|
||||
Toast.show(message, {
|
||||
duration: Toast.durations.LONG,
|
||||
position: 50,
|
||||
shadow: true,
|
||||
animation: true,
|
||||
hideOnPress: true,
|
||||
})
|
||||
return (
|
||||
<>
|
||||
{activeToast && (
|
||||
<View dataSet={{'toast-container': 1}}>
|
||||
<FontAwesomeIcon icon="check" size={24} />
|
||||
<Text>{activeToast.text}</Text>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// exports
|
||||
// =
|
||||
export default {
|
||||
show(text: string, _opts: any) {
|
||||
console.log('TODO: toast', text)
|
||||
if (toastTimeout) {
|
||||
clearTimeout(toastTimeout)
|
||||
}
|
||||
globalSetActiveToast?.({text})
|
||||
toastTimeout = setTimeout(() => {
|
||||
globalSetActiveToast?.(undefined)
|
||||
}, 2e3)
|
||||
},
|
||||
positions: {
|
||||
TOP: 0,
|
||||
},
|
||||
durations: {
|
||||
LONG: 0,
|
||||
},
|
||||
ToastContainer,
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {UserAvatar} from './UserAvatar'
|
||||
import {colors} from '../../lib/styles'
|
||||
import {MagnifyingGlassIcon} from '../../lib/icons'
|
||||
import {useStores} from '../../../state'
|
||||
|
@ -9,14 +8,19 @@ import {useStores} from '../../../state'
|
|||
export function ViewHeader({
|
||||
title,
|
||||
subtitle,
|
||||
onPost,
|
||||
}: {
|
||||
title: string
|
||||
subtitle?: string
|
||||
onPost?: () => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
const onPressBack = () => {
|
||||
store.nav.tab.goBack()
|
||||
}
|
||||
const onPressCompose = () => {
|
||||
store.shell.openComposer({onPost})
|
||||
}
|
||||
const onPressSearch = () => {
|
||||
store.nav.navigate(`/search`)
|
||||
}
|
||||
|
@ -26,9 +30,7 @@ export function ViewHeader({
|
|||
<TouchableOpacity onPress={onPressBack} style={styles.backIcon}>
|
||||
<FontAwesomeIcon size={18} icon="angle-left" style={{marginTop: 6}} />
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View style={styles.cornerPlaceholder} />
|
||||
)}
|
||||
) : undefined}
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
{subtitle ? (
|
||||
|
@ -37,8 +39,17 @@ export function ViewHeader({
|
|||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
<TouchableOpacity onPress={onPressSearch} style={styles.searchBtn}>
|
||||
<MagnifyingGlassIcon size={17} style={styles.searchBtnIcon} />
|
||||
<TouchableOpacity onPress={onPressCompose} style={styles.btn}>
|
||||
<FontAwesomeIcon size={18} icon="plus" />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={onPressSearch}
|
||||
style={[styles.btn, {marginLeft: 8}]}>
|
||||
<MagnifyingGlassIcon
|
||||
size={18}
|
||||
strokeWidth={3}
|
||||
style={styles.searchBtnIcon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
|
@ -59,33 +70,28 @@ const styles = StyleSheet.create({
|
|||
titleContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontSize: 21,
|
||||
fontWeight: '600',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 15,
|
||||
marginLeft: 3,
|
||||
fontSize: 18,
|
||||
marginLeft: 6,
|
||||
color: colors.gray4,
|
||||
maxWidth: 200,
|
||||
},
|
||||
|
||||
cornerPlaceholder: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
},
|
||||
backIcon: {width: 30, height: 30},
|
||||
searchBtn: {
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.gray1,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 20,
|
||||
},
|
||||
searchBtnIcon: {
|
||||
color: colors.black,
|
||||
|
|
|
@ -94,15 +94,17 @@ export function HomeIconSolid({
|
|||
export function MagnifyingGlassIcon({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 2,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={2}
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
|
|
|
@ -47,6 +47,7 @@ export const Home = observer(function Home({
|
|||
if (!visible) {
|
||||
return
|
||||
}
|
||||
|
||||
if (hasSetup) {
|
||||
console.log('Updating home feed')
|
||||
defaultFeedView.update()
|
||||
|
@ -80,7 +81,11 @@ export const Home = observer(function Home({
|
|||
|
||||
return (
|
||||
<View style={s.flex1}>
|
||||
<ViewHeader title="Bluesky" subtitle="Private Beta" />
|
||||
<ViewHeader
|
||||
title="Bluesky"
|
||||
subtitle="Private Beta"
|
||||
onPost={onCreatePost}
|
||||
/>
|
||||
<Feed
|
||||
key="default"
|
||||
feed={defaultFeedView}
|
||||
|
@ -106,8 +111,8 @@ const styles = StyleSheet.create({
|
|||
left: 10,
|
||||
bottom: 15,
|
||||
backgroundColor: colors.pink3,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 30,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.3,
|
||||
|
@ -117,5 +122,6 @@ const styles = StyleSheet.create({
|
|||
color: colors.white,
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 5,
|
||||
fontSize: 16,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -15,7 +15,8 @@ import {PostFeedLoadingPlaceholder} from '../com/util/LoadingPlaceholder'
|
|||
import {ErrorScreen} from '../com/util/ErrorScreen'
|
||||
import {ErrorMessage} from '../com/util/ErrorMessage'
|
||||
import {EmptyState} from '../com/util/EmptyState'
|
||||
import Toast from '../com/util/Toast'
|
||||
import {ViewHeader} from '../com/util/ViewHeader'
|
||||
import * as Toast from '../com/util/Toast'
|
||||
import {s, colors} from '../lib/styles'
|
||||
|
||||
const LOADING_ITEM = {_reactKey: '__loading__'}
|
||||
|
@ -77,10 +78,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
|
|||
`You'll be able to invite them again if you change your mind.`,
|
||||
async () => {
|
||||
await uiState.members.removeMember(membership.did)
|
||||
Toast.show(`User removed`, {
|
||||
duration: Toast.durations.LONG,
|
||||
position: Toast.positions.TOP,
|
||||
})
|
||||
Toast.show(`User removed`)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
@ -219,8 +217,11 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
|
|||
renderItem = () => <View />
|
||||
}
|
||||
|
||||
const title =
|
||||
uiState.profile.displayName || uiState.profile.handle || params.name
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ViewHeader title={title} />
|
||||
{uiState.profile.hasError ? (
|
||||
<ErrorScreen
|
||||
title="Failed to load profile"
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
|
@ -25,10 +24,17 @@ import {CreateSceneModel} from '../../../state/models/shell-ui'
|
|||
import {s, colors} from '../../lib/styles'
|
||||
|
||||
export const MainMenu = observer(
|
||||
({active, onClose}: {active: boolean; onClose: () => void}) => {
|
||||
({
|
||||
active,
|
||||
insetBottom,
|
||||
onClose,
|
||||
}: {
|
||||
active: boolean
|
||||
insetBottom: number
|
||||
onClose: () => void
|
||||
}) => {
|
||||
const store = useStores()
|
||||
const initInterp = useSharedValue<number>(0)
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
|
@ -172,7 +178,7 @@ export const MainMenu = observer(
|
|||
<Animated.View
|
||||
style={[
|
||||
styles.wrapper,
|
||||
{bottom: insets.bottom + 55},
|
||||
{bottom: insetBottom + 45},
|
||||
wrapperAnimStyle,
|
||||
]}>
|
||||
<SafeAreaView>
|
||||
|
@ -267,7 +273,8 @@ const styles = StyleSheet.create({
|
|||
alignItems: 'center',
|
||||
height: 40,
|
||||
paddingHorizontal: 10,
|
||||
marginBottom: 16,
|
||||
marginTop: 12,
|
||||
marginBottom: 20,
|
||||
},
|
||||
section: {
|
||||
paddingHorizontal: 10,
|
||||
|
|
|
@ -70,7 +70,7 @@ const Btn = ({
|
|||
onPress?: (event: GestureResponderEvent) => void
|
||||
onLongPress?: (event: GestureResponderEvent) => void
|
||||
}) => {
|
||||
let size = 21
|
||||
let size = 24
|
||||
let addedStyles
|
||||
let IconEl
|
||||
if (icon === 'menu') {
|
||||
|
@ -79,17 +79,17 @@ const Btn = ({
|
|||
IconEl = GridIconSolid
|
||||
} else if (icon === 'home') {
|
||||
IconEl = HomeIcon
|
||||
size = 24
|
||||
size = 27
|
||||
} else if (icon === 'home-solid') {
|
||||
IconEl = HomeIconSolid
|
||||
size = 24
|
||||
size = 27
|
||||
} else if (icon === 'bell') {
|
||||
IconEl = BellIcon
|
||||
size = 24
|
||||
size = 27
|
||||
addedStyles = {position: 'relative', top: -1} as ViewStyle
|
||||
} else if (icon === 'bell-solid') {
|
||||
IconEl = BellIconSolid
|
||||
size = 24
|
||||
size = 27
|
||||
addedStyles = {position: 'relative', top: -1} as ViewStyle
|
||||
} else {
|
||||
IconEl = FontAwesomeIcon
|
||||
|
@ -316,7 +316,7 @@ export const MobileShell: React.FC = observer(() => {
|
|||
<View
|
||||
style={[
|
||||
styles.bottomBar,
|
||||
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
|
||||
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 40)},
|
||||
]}>
|
||||
<Btn
|
||||
icon={isAtHome ? 'home-solid' : 'home'}
|
||||
|
@ -343,6 +343,7 @@ export const MobileShell: React.FC = observer(() => {
|
|||
</View>
|
||||
<MainMenu
|
||||
active={isMainMenuActive}
|
||||
insetBottom={clamp(safeAreaInsets.bottom, 15, 40)}
|
||||
onClose={() => setMainMenuActive(false)}
|
||||
/>
|
||||
<Modal />
|
||||
|
@ -491,7 +492,7 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
ctrl: {
|
||||
flex: 1,
|
||||
paddingTop: 15,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 5,
|
||||
},
|
||||
notificationCount: {
|
||||
|
|
|
@ -11718,6 +11718,11 @@ thunky@^1.0.2:
|
|||
resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d"
|
||||
integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==
|
||||
|
||||
tlds@^1.234.0:
|
||||
version "1.234.0"
|
||||
resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.234.0.tgz#f61fe73f6e85c51f8503181f47dcfbd18c6910db"
|
||||
integrity sha512-TNDfeyDIC+oroH44bMbWC+Jn/2qNrfRvDK2EXt1icOXYG5NMqoRyUosADrukfb4D8lJ3S1waaBWSvQro0erdng==
|
||||
|
||||
tmpl@1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
|
||||
|
|
Loading…
Reference in New Issue