Merge branch 'main' into upload-image

zio/stable
João Ferreiro 2022-11-28 16:56:05 +00:00
commit c5f3200d6b
27 changed files with 424 additions and 428 deletions

View File

@ -31,6 +31,11 @@ describe('extractEntities', () => {
'start middle end.com/foo/bar?baz=bux#hash', 'start middle end.com/foo/bar?baz=bux#hash',
'newline1.com\nnewline2.com', 'newline1.com\nnewline2.com',
'not.. a..url ..here', 'not.. a..url ..here',
'e.g.',
'something-cool.jpg',
'website.com.jpg',
'e.g./foo',
'website.com.jpg/foo',
] ]
interface Output { interface Output {
type: string type: string
@ -80,6 +85,11 @@ describe('extractEntities', () => {
{type: 'link', value: 'newline2.com', noScheme: true}, {type: 'link', value: 'newline2.com', noScheme: true},
], ],
[], [],
[],
[],
[],
[],
[],
] ]
it('correctly handles a set of text inputs', () => { it('correctly handles a set of text inputs', () => {
for (let i = 0; i < inputs.length; i++) { for (let i = 0; i < inputs.length; i++) {
@ -145,6 +155,12 @@ describe('detectLinkables', () => {
'start middle end.com/foo/bar?baz=bux#hash', 'start middle end.com/foo/bar?baz=bux#hash',
'newline1.com\nnewline2.com', 'newline1.com\nnewline2.com',
'not.. a..url ..here', '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 = [ const outputs = [
['no linkable'], ['no linkable'],
@ -171,6 +187,12 @@ describe('detectLinkables', () => {
['start middle ', {link: 'end.com/foo/bar?baz=bux#hash'}], ['start middle ', {link: 'end.com/foo/bar?baz=bux#hash'}],
[{link: 'newline1.com'}, '\n', {link: 'newline2.com'}], [{link: 'newline1.com'}, '\n', {link: 'newline2.com'}],
['not.. a..url ..here'], ['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', () => { it('correctly handles a set of text inputs', () => {
for (let i = 0; i < inputs.length; i++) { for (let i = 0; i < inputs.length; i++) {

View File

@ -48,7 +48,8 @@
"react-native-svg": "^12.4.0", "react-native-svg": "^12.4.0",
"react-native-tab-view": "^3.3.0", "react-native-tab-view": "^3.3.0",
"react-native-url-polyfill": "^1.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": { "devDependencies": {
"@babel/core": "^7.12.9", "@babel/core": "^7.12.9",

View File

@ -2,7 +2,7 @@ import React, {useState, useEffect} from 'react'
import * as view from './view/index' import * as view from './view/index'
import {RootStoreModel, setupState, RootStoreProvider} from './state' import {RootStoreModel, setupState, RootStoreProvider} from './state'
import {DesktopWebShell} from './view/shell/desktop-web' import {DesktopWebShell} from './view/shell/desktop-web'
import Toast from './view/com/util/Toast' import Toast from 'react-native-root-toast'
function App() { function App() {
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>( const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(

View File

@ -1,6 +1,7 @@
import {AtUri} from '../third-party/uri' import {AtUri} from '../third-party/uri'
import {Entity} from '../third-party/api/src/client/types/app/bsky/feed/post' import {Entity} from '../third-party/api/src/client/types/app/bsky/feed/post'
import {PROD_SERVICE} from '../state' import {PROD_SERVICE} from '../state'
import TLDs from 'tlds'
export const MAX_DISPLAY_NAME = 64 export const MAX_DISPLAY_NAME = 64
export const MAX_DESCRIPTION = 256 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( export function extractEntities(
text: string, text: string,
knownHandles?: Set<string>, knownHandles?: Set<string>,
@ -85,10 +94,14 @@ export function extractEntities(
{ {
// links // links
const re = 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))) { while ((match = re.exec(text))) {
let value = match[2] let value = match[2]
if (!value.startsWith('http')) { if (!value.startsWith('http')) {
const domain = match.groups?.domain
if (!domain || !isValidDomain(domain)) {
continue
}
value = `https://${value}` value = `https://${value}`
} }
ents.push({ ents.push({
@ -110,7 +123,7 @@ interface DetectedLink {
type DetectedLinkable = string | DetectedLink type DetectedLinkable = string | DetectedLink
export function detectLinkables(text: string): DetectedLinkable[] { export function detectLinkables(text: string): DetectedLinkable[] {
const re = 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 = [] const segments = []
let match let match
let start = 0 let start = 0
@ -118,6 +131,10 @@ export function detectLinkables(text: string): DetectedLinkable[] {
let matchIndex = match.index let matchIndex = match.index
let matchValue = match[0] let matchValue = match[0]
if (match.groups?.domain && !isValidDomain(match.groups?.domain)) {
continue
}
if (/\s/.test(matchValue)) { if (/\s/.test(matchValue)) {
// HACK // HACK
// skip the starting space // skip the starting space

View File

@ -7,6 +7,8 @@ import * as apilib from '../lib/api'
import {cleanError} from '../../lib/strings' import {cleanError} from '../../lib/strings'
import {isObj, hasProp} from '../lib/type-guards' import {isObj, hasProp} from '../lib/type-guards'
const PAGE_SIZE = 30
type FeedItem = GetTimeline.FeedItem | GetAuthorFeed.FeedItem type FeedItem = GetTimeline.FeedItem | GetAuthorFeed.FeedItem
type FeedItemWithThreadMeta = FeedItem & { type FeedItemWithThreadMeta = FeedItem & {
_isThreadParent?: boolean _isThreadParent?: boolean
@ -166,6 +168,7 @@ export class FeedModel {
params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams
hasMore = true hasMore = true
loadMoreCursor: string | undefined loadMoreCursor: string | undefined
pollCursor: string | undefined
_loadPromise: Promise<void> | undefined _loadPromise: Promise<void> | undefined
_loadMorePromise: Promise<void> | undefined _loadMorePromise: Promise<void> | undefined
_loadLatestPromise: Promise<void> | undefined _loadLatestPromise: Promise<void> | undefined
@ -300,7 +303,7 @@ export class FeedModel {
const res = await this._getFeed({limit: 1}) const res = await this._getFeed({limit: 1})
this.setHasNewLatest( this.setHasNewLatest(
res.data.feed[0] && 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) { private async _initialLoad(isRefreshing = false) {
this._xLoading(isRefreshing) this._xLoading(isRefreshing)
try { try {
const res = await this._getFeed() const res = await this._getFeed({limit: PAGE_SIZE})
this._replaceAll(res) this._replaceAll(res)
this._xIdle() this._xIdle()
} catch (e: any) { } catch (e: any) {
@ -352,7 +355,7 @@ export class FeedModel {
private async _loadLatest() { private async _loadLatest() {
this._xLoading() this._xLoading()
try { try {
const res = await this._getFeed() const res = await this._getFeed({limit: PAGE_SIZE})
this._prependAll(res) this._prependAll(res)
this._xIdle() this._xIdle()
} catch (e: any) { } catch (e: any) {
@ -368,6 +371,7 @@ export class FeedModel {
try { try {
const res = await this._getFeed({ const res = await this._getFeed({
before: this.loadMoreCursor, before: this.loadMoreCursor,
limit: PAGE_SIZE,
}) })
this._appendAll(res) this._appendAll(res)
this._xIdle() this._xIdle()
@ -402,6 +406,7 @@ export class FeedModel {
private _replaceAll(res: GetTimeline.Response | GetAuthorFeed.Response) { private _replaceAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
this.feed.length = 0 this.feed.length = 0
this.pollCursor = res.data.feed[0]?.uri
this._appendAll(res) this._appendAll(res)
} }
@ -434,6 +439,7 @@ export class FeedModel {
} }
private _prependAll(res: GetTimeline.Response | GetAuthorFeed.Response) { private _prependAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
this.pollCursor = res.data.feed[0]?.uri
let counter = this.feed.length let counter = this.feed.length
const toPrepend = [] const toPrepend = []
for (const item of res.data.feed) { for (const item of res.data.feed) {
@ -493,8 +499,7 @@ function preprocessFeed(
for (let i = feed.length - 1; i >= 0; i--) { for (let i = feed.length - 1; i >= 0; i--) {
const item = feed[i] as FeedItemWithThreadMeta const item = feed[i] as FeedItemWithThreadMeta
// dont dedup the first item so that polling works properly if (dedup) {
if (dedup && i !== 0) {
if (reorg.find(item2 => item2.uri === item.uri)) { if (reorg.find(item2 => item2.uri === item.uri)) {
continue continue
} }

View File

@ -7,7 +7,7 @@ import {APP_BSKY_GRAPH} from '../../third-party/api'
import {cleanError} from '../../lib/strings' import {cleanError} from '../../lib/strings'
const UNGROUPABLE_REASONS = ['trend', 'assertion'] const UNGROUPABLE_REASONS = ['trend', 'assertion']
const PAGE_SIZE = 30
const MS_60MIN = 1e3 * 60 * 60 const MS_60MIN = 1e3 * 60 * 60
export interface GroupedNotification extends ListNotifications.Notification { export interface GroupedNotification extends ListNotifications.Notification {
@ -242,9 +242,10 @@ export class NotificationsViewModel {
private async _initialLoad(isRefreshing = false) { private async _initialLoad(isRefreshing = false) {
this._xLoading(isRefreshing) this._xLoading(isRefreshing)
try { try {
const res = await this.rootStore.api.app.bsky.notification.list( const params = Object.assign({}, this.params, {
this.params, limit: PAGE_SIZE,
) })
const res = await this.rootStore.api.app.bsky.notification.list(params)
this._replaceAll(res) this._replaceAll(res)
this._xIdle() this._xIdle()
} catch (e: any) { } catch (e: any) {
@ -259,6 +260,7 @@ export class NotificationsViewModel {
this._xLoading() this._xLoading()
try { try {
const params = Object.assign({}, this.params, { const params = Object.assign({}, this.params, {
limit: PAGE_SIZE,
before: this.loadMoreCursor, before: this.loadMoreCursor,
}) })
const res = await this.rootStore.api.app.bsky.notification.list(params) const res = await this.rootStore.api.app.bsky.notification.list(params)

View File

@ -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 {observer} from 'mobx-react-lite'
import { import {
ActivityIndicator, ActivityIndicator,
@ -17,9 +17,10 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view' import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view'
import {UserLocalPhotosModel} from '../../../state/models/user-local-photos' import {UserLocalPhotosModel} from '../../../state/models/user-local-photos'
import {Autocomplete} from './Autocomplete' import {Autocomplete} from './Autocomplete'
import Toast from '../util/Toast' import * as Toast from '../util/Toast'
import ProgressCircle from '../util/ProgressCircle' import ProgressCircle from '../util/ProgressCircle'
import {TextLink} from '../util/Link' import {TextLink} from '../util/Link'
import {UserAvatar} from '../util/UserAvatar'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import * as apilib from '../../../state/lib/api' import * as apilib from '../../../state/lib/api'
import {ComposerOpts} from '../../../state/models/shell-ui' 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' import {openPicker, openCamera} from 'react-native-image-crop-picker'
const MAX_TEXT_LENGTH = 256 const MAX_TEXT_LENGTH = 256
const WARNING_TEXT_LENGTH = 200
const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH
export const ComposePost = observer(function ComposePost({ export const ComposePost = observer(function ComposePost({
@ -41,6 +41,7 @@ export const ComposePost = observer(function ComposePost({
onClose: () => void onClose: () => void
}) { }) {
const store = useStores() const store = useStores()
const textInput = useRef<TextInput>(null)
const [isProcessing, setIsProcessing] = useState(false) const [isProcessing, setIsProcessing] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
const [text, setText] = useState('') const [text, setText] = useState('')
@ -57,6 +58,22 @@ export const ComposePost = observer(function ComposePost({
useEffect(() => { useEffect(() => {
autocompleteView.setup() 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(() => { useEffect(() => {
localPhotos.setup() localPhotos.setup()
@ -90,7 +107,10 @@ export const ComposePost = observer(function ComposePost({
} }
setIsProcessing(true) setIsProcessing(true)
try { 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) { } catch (e: any) {
console.error(`Failed to create post: ${e.toString()}`) console.error(`Failed to create post: ${e.toString()}`)
setError( setError(
@ -101,13 +121,7 @@ export const ComposePost = observer(function ComposePost({
} }
onPost?.() onPost?.()
onClose() onClose()
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`, { Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
shadow: true,
animation: true,
hideOnPress: true,
})
} }
const onSelectAutocompleteItem = (item: string) => { const onSelectAutocompleteItem = (item: string) => {
setText(replaceTextAutocompletePrefix(text, item)) setText(replaceTextAutocompletePrefix(text, item))
@ -115,12 +129,7 @@ export const ComposePost = observer(function ComposePost({
} }
const canPost = text.length <= MAX_TEXT_LENGTH const canPost = text.length <= MAX_TEXT_LENGTH
const progressColor = const progressColor = text.length > DANGER_TEXT_LENGTH ? '#e60000' : undefined
text.length > DANGER_TEXT_LENGTH
? '#e60000'
: text.length > WARNING_TEXT_LENGTH
? '#f7c600'
: undefined
const textDecorated = useMemo(() => { const textDecorated = useMemo(() => {
let i = 0 let i = 0
@ -142,7 +151,7 @@ export const ComposePost = observer(function ComposePost({
<SafeAreaView style={s.flex1}> <SafeAreaView style={s.flex1}>
<View style={styles.topbar}> <View style={styles.topbar}>
<TouchableOpacity onPress={onPressCancel}> <TouchableOpacity onPress={onPressCancel}>
<Text style={[s.blue3, s.f16]}>Cancel</Text> <Text style={[s.blue3, s.f18]}>Cancel</Text>
</TouchableOpacity> </TouchableOpacity>
<View style={s.flex1} /> <View style={s.flex1} />
{isProcessing ? ( {isProcessing ? (
@ -156,7 +165,9 @@ export const ComposePost = observer(function ComposePost({
start={{x: 0, y: 0}} start={{x: 0, y: 0}}
end={{x: 1, y: 1}} end={{x: 1, y: 1}}
style={styles.postBtn}> 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> </LinearGradient>
</TouchableOpacity> </TouchableOpacity>
) : ( ) : (
@ -178,39 +189,46 @@ export const ComposePost = observer(function ComposePost({
</View> </View>
)} )}
{replyTo ? ( {replyTo ? (
<View> <View style={styles.replyToLayout}>
<Text style={s.gray4}> <UserAvatar
Replying to{' '} handle={replyTo.author.handle}
displayName={replyTo.author.displayName}
size={50}
/>
<View style={styles.replyToPost}>
<TextLink <TextLink
href={`/profile/${replyTo.author.handle}`} href={`/profile/${replyTo.author.handle}`}
text={'@' + replyTo.author.handle} text={replyTo.author.displayName || replyTo.author.handle}
style={[s.bold, s.gray5]} style={[s.f16, s.bold]}
/> />
</Text> <Text style={[s.f16, s['lh16-1.3']]} numberOfLines={6}>
<View style={styles.replyToPost}> {replyTo.text}
<Text style={s.gray5}>{replyTo.text}</Text> </Text>
</View> </View>
</View> </View>
) : undefined} ) : undefined}
<TextInput <View style={styles.textInputLayout}>
multiline <UserAvatar
scrollEnabled handle={store.me.handle || ''}
onChangeText={(text: string) => onChangeText(text)} displayName={store.me.displayName}
placeholder={ size={50}
replyTo />
? 'Write your reply' <TextInput
: photoUris.length === 0 ref={textInput}
? "What's up?" multiline
: 'Add a comment...' scrollEnabled
} onChangeText={(text: string) => onChangeText(text)}
style={styles.textInput}> placeholder={replyTo ? 'Write your reply' : "What's up?"}
{textDecorated} style={styles.textInput}>
</TextInput> {textDecorated}
</TextInput>
</View>
{photoUris.length !== 0 && ( {photoUris.length !== 0 && (
<View style={styles.selectedImageContainer}> <View style={styles.selectedImageContainer}>
{photoUris.length !== 0 && {photoUris.length !== 0 &&
photoUris.map(item => ( photoUris.map((item, index) => (
<View <View
key={`selected-image-${index}`}
style={[ style={[
styles.selectedImage, styles.selectedImage,
photoUris.length === 1 photoUris.length === 1
@ -264,8 +282,9 @@ export const ComposePost = observer(function ComposePost({
style={{color: colors.blue3}} style={{color: colors.blue3}}
/> />
</TouchableOpacity> </TouchableOpacity>
{localPhotos.photos.map(item => ( {localPhotos.photos.map((item, index) => (
<TouchableOpacity <TouchableOpacity
key={`local-image-${index}`}
style={styles.photoButton} style={styles.photoButton}
onPress={() => { onPress={() => {
setPhotoUris([item.node.image.uri, ...photoUris]) setPhotoUris([item.node.image.uri, ...photoUris])
@ -343,9 +362,9 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingTop: 10, paddingTop: 10,
paddingBottom: 5, paddingBottom: 10,
paddingHorizontal: 5, paddingHorizontal: 5,
height: 50, height: 55,
}, },
postBtn: { postBtn: {
borderRadius: 20, borderRadius: 20,
@ -371,19 +390,30 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
marginRight: 5, marginRight: 5,
}, },
textInputLayout: {
flexDirection: 'row',
flex: 1,
borderTopWidth: 1,
borderTopColor: colors.gray2,
paddingTop: 16,
},
textInput: { textInput: {
flex: 1, flex: 1,
padding: 5, padding: 5,
fontSize: 21, fontSize: 18,
marginLeft: 8,
},
replyToLayout: {
flexDirection: 'row',
borderTopWidth: 1,
borderTopColor: colors.gray2,
paddingTop: 16,
paddingBottom: 16,
}, },
replyToPost: { replyToPost: {
paddingHorizontal: 8, flex: 1,
paddingVertical: 6, paddingLeft: 13,
borderWidth: 1, paddingRight: 8,
borderColor: colors.gray2,
borderRadius: 6,
marginTop: 5,
marginBottom: 10,
}, },
contentCenter: {alignItems: 'center'}, contentCenter: {alignItems: 'center'},
selectedImageContainer: { selectedImageContainer: {

View File

@ -1,29 +1,42 @@
import React from 'react' import React from 'react'
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {colors} from '../../lib/styles' import {colors} from '../../lib/styles'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {UserAvatar} from '../util/UserAvatar' 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 store = useStores()
const onPressAvatar = () => { const onPressAvatar = () => {
store.nav.navigate(`/profile/${store.me.handle}`) store.nav.navigate(`/profile/${store.me.handle}`)
} }
return ( return (
<TouchableOpacity style={styles.container} onPress={onPressCompose}> <TouchableOpacity
<TouchableOpacity style={styles.avatar} onPress={onPressAvatar}> style={[styles.container, noAvi ? styles.noAviContainer : undefined]}
<UserAvatar onPress={onPressCompose}>
size={50} {!noAvi ? (
handle={store.me.handle || ''} <TouchableOpacity style={styles.avatar} onPress={onPressAvatar}>
displayName={store.me.displayName} <UserAvatar
/> size={50}
</TouchableOpacity> handle={store.me.handle || ''}
displayName={store.me.displayName}
/>
</TouchableOpacity>
) : undefined}
<View style={styles.textContainer}> <View style={styles.textContainer}>
<Text style={styles.text}>What's up?</Text> <Text style={styles.text}>{text}</Text>
</View> </View>
<View style={styles.btn}> <View style={styles.btn}>
<Text style={styles.btnText}>Post</Text> <Text style={styles.btnText}>{btn}</Text>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
) )
@ -40,6 +53,9 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.white, backgroundColor: colors.white,
}, },
noAviContainer: {
paddingVertical: 14,
},
avatar: { avatar: {
width: 50, width: 50,
}, },

View File

@ -14,7 +14,7 @@ import _omit from 'lodash.omit'
import {ErrorScreen} from '../util/ErrorScreen' import {ErrorScreen} from '../util/ErrorScreen'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import Toast from '../util/Toast' import * as Toast from '../util/Toast'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import * as apilib from '../../../state/lib/api' import * as apilib from '../../../state/lib/api'
import { import {
@ -63,10 +63,7 @@ export const SuggestedFollows = observer(
setFollows({[item.did]: res.uri, ...follows}) setFollows({[item.did]: res.uri, ...follows})
} catch (e) { } catch (e) {
console.log(e) console.log(e)
Toast.show('An issue occurred, please try again.', { Toast.show('An issue occurred, please try again.')
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
})
} }
} }
const onPressUnfollow = async (item: SuggestedActor) => { const onPressUnfollow = async (item: SuggestedActor) => {
@ -75,10 +72,7 @@ export const SuggestedFollows = observer(
setFollows(_omit(follows, [item.did])) setFollows(_omit(follows, [item.did]))
} catch (e) { } catch (e) {
console.log(e) console.log(e)
Toast.show('An issue occurred, please try again.', { Toast.show('An issue occurred, please try again.')
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
})
} }
} }

View File

@ -1,5 +1,5 @@
import React, {useState} from 'react' import React, {useState} from 'react'
import Toast from '../util/Toast' import * as Toast from '../util/Toast'
import { import {
ActivityIndicator, ActivityIndicator,
StyleSheet, StyleSheet,
@ -71,9 +71,7 @@ export function Component({}: {}) {
}, },
) )
.catch(e => console.error(e)) // an error here is not critical .catch(e => console.error(e)) // an error here is not critical
Toast.show('Scene created', { Toast.show('Scene created')
position: Toast.positions.TOP,
})
store.shell.closeModal() store.shell.closeModal()
store.nav.navigate(`/profile/${fullHandle}`) store.nav.navigate(`/profile/${fullHandle}`)
} catch (e: any) { } catch (e: any) {

View File

@ -1,5 +1,5 @@
import React, {useState} from 'react' 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 {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
import LinearGradient from 'react-native-linear-gradient' import LinearGradient from 'react-native-linear-gradient'
import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet' import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet'
@ -52,9 +52,7 @@ export function Component({
} }
}, },
) )
Toast.show('Profile updated', { Toast.show('Profile updated')
position: Toast.positions.TOP,
})
onUpdate?.() onUpdate?.()
store.shell.closeModal() store.shell.closeModal()
} catch (e: any) { } catch (e: any) {

View File

@ -1,6 +1,6 @@
import React, {useState, useEffect, useMemo} from 'react' import React, {useState, useEffect, useMemo} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import Toast from '../util/Toast' import * as Toast from '../util/Toast'
import { import {
ActivityIndicator, ActivityIndicator,
FlatList, FlatList,
@ -83,10 +83,7 @@ export const Component = observer(function Component({
follow.declaration.cid, follow.declaration.cid,
) )
setCreatedInvites({[follow.did]: assertionUri, ...createdInvites}) setCreatedInvites({[follow.did]: assertionUri, ...createdInvites})
Toast.show('Invite sent', { Toast.show('Invite sent')
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
})
} catch (e) { } catch (e) {
setError('There was an issue with the invite. Please try again.') setError('There was an issue with the invite. Please try again.')
console.error(e) console.error(e)
@ -119,10 +116,7 @@ export const Component = observer(function Component({
[assertion.uri]: true, [assertion.uri]: true,
...deletedPendingInvites, ...deletedPendingInvites,
}) })
Toast.show('Invite removed', { Toast.show('Invite removed')
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
})
} catch (e) { } catch (e) {
setError('There was an issue with the invite. Please try again.') setError('There was an issue with the invite. Please try again.')
console.error(e) console.error(e)

View File

@ -7,7 +7,7 @@ import {NotificationsViewItemModel} from '../../../state/models/notifications-vi
import {ConfirmModel} from '../../../state/models/shell-ui' import {ConfirmModel} from '../../../state/models/shell-ui'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {ProfileCard} from '../profile/ProfileCard' import {ProfileCard} from '../profile/ProfileCard'
import Toast from '../util/Toast' import * as Toast from '../util/Toast'
import {s, colors, gradients} from '../../lib/styles' import {s, colors, gradients} from '../../lib/styles'
export function InviteAccepter({item}: {item: NotificationsViewItemModel}) { export function InviteAccepter({item}: {item: NotificationsViewItemModel}) {
@ -46,10 +46,7 @@ export function InviteAccepter({item}: {item: NotificationsViewItemModel}) {
}, },
}) })
store.me.refreshMemberships() store.me.refreshMemberships()
Toast.show('Invite accepted', { Toast.show('Invite accepted')
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
})
setConfirmationUri(uri) setConfirmationUri(uri)
} }
return ( return (

View File

@ -8,7 +8,7 @@ import {PostThreadViewPostModel} from '../../../state/models/post-thread-view'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {RichText} from '../util/RichText' import {RichText} from '../util/RichText'
import {PostDropdownBtn} from '../util/DropdownBtn' import {PostDropdownBtn} from '../util/DropdownBtn'
import Toast from '../util/Toast' import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
import {ago, pluralize} from '../../../lib/strings' import {ago, pluralize} from '../../../lib/strings'
@ -16,6 +16,7 @@ import {useStores} from '../../../state'
import {PostMeta} from '../util/PostMeta' import {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/PostEmbeds' import {PostEmbeds} from '../util/PostEmbeds'
import {PostCtrls} from '../util/PostCtrls' import {PostCtrls} from '../util/PostCtrls'
import {ComposePrompt} from '../composer/Prompt'
const PARENT_REPLY_LINE_LENGTH = 8 const PARENT_REPLY_LINE_LENGTH = 8
const REPLYING_TO_LINE_LENGTH = 6 const REPLYING_TO_LINE_LENGTH = 6
@ -78,131 +79,133 @@ export const PostThreadItem = observer(function PostThreadItem({
item.delete().then( item.delete().then(
() => { () => {
setDeleted(true) setDeleted(true)
Toast.show('Post deleted', { Toast.show('Post deleted')
position: Toast.positions.TOP,
})
}, },
e => { e => {
console.error(e) console.error(e)
Toast.show('Failed to delete post, please try again', { Toast.show('Failed to delete post, please try again')
position: Toast.positions.TOP,
})
}, },
) )
} }
if (item._isHighlightedPost) { if (item._isHighlightedPost) {
return ( return (
<View style={styles.outer}> <>
<View style={styles.layout}> <View style={styles.outer}>
<View style={styles.layoutAvi}> <View style={styles.layout}>
<Link href={authorHref} title={authorTitle}> <View style={styles.layoutAvi}>
<UserAvatar <Link href={authorHref} title={authorTitle}>
size={50} <UserAvatar
displayName={item.author.displayName} size={50}
handle={item.author.handle} 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]}>
&middot; {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.metaItem}
href={authorHref}
title={authorTitle}>
<Text style={[s.f15, s.gray5]} numberOfLines={1}>
@{item.author.handle}
</Text>
</Link> </Link>
</View> </View>
</View> <View style={styles.layoutContent}>
</View> <View style={[styles.meta, {paddingTop: 5, paddingBottom: 0}]}>
<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 ? (
<Link <Link
style={styles.expandedInfoItem} style={styles.metaItem}
href={repostsHref} href={authorHref}
title={repostsTitle}> title={authorTitle}>
<Text style={[s.gray5, s.semiBold, s.f18]}> <Text style={[s.f16, s.bold]} numberOfLines={1}>
<Text style={[s.bold, s.black, s.f18]}> {item.author.displayName || item.author.handle}
{item.repostCount}
</Text>{' '}
{pluralize(item.repostCount, 'repost')}
</Text> </Text>
</Link> </Link>
) : ( <Text style={[styles.metaItem, s.f15, s.gray5]}>
<></> &middot; {ago(item.indexedAt)}
)} </Text>
{item.upvoteCount ? ( <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 <Link
style={styles.expandedInfoItem} style={styles.metaItem}
href={upvotesHref} href={authorHref}
title={upvotesTitle}> title={authorTitle}>
<Text style={[s.gray5, s.semiBold, s.f18]}> <Text style={[s.f15, s.gray5]} numberOfLines={1}>
<Text style={[s.bold, s.black, s.f18]}> @{item.author.handle}
{item.upvoteCount}
</Text>{' '}
{pluralize(item.upvoteCount, 'upvote')}
</Text> </Text>
</Link> </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>
) : (
<></>
)}
<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> </View>
</View> <ComposePrompt
noAvi
text="Write your reply"
btn="Reply"
onPressCompose={onPressReply}
/>
</>
) )
} else { } else {
return ( return (
@ -345,8 +348,8 @@ const styles = StyleSheet.create({
}, },
postText: { postText: {
fontFamily: 'Helvetica Neue', fontFamily: 'Helvetica Neue',
fontSize: 17, fontSize: 16,
lineHeight: 22.1, // 1.3 of 17px lineHeight: 20.8, // 1.3 of 16px
}, },
postTextContainer: { postTextContainer: {
flexDirection: 'row', flexDirection: 'row',
@ -371,7 +374,7 @@ const styles = StyleSheet.create({
borderTopWidth: 1, borderTopWidth: 1,
borderBottomWidth: 1, borderBottomWidth: 1,
marginTop: 5, marginTop: 5,
marginBottom: 10, marginBottom: 15,
}, },
expandedInfoItem: { expandedInfoItem: {
marginRight: 10, marginRight: 10,

View File

@ -10,7 +10,7 @@ import {UserInfoText} from '../util/UserInfoText'
import {PostMeta} from '../util/PostMeta' import {PostMeta} from '../util/PostMeta'
import {PostCtrls} from '../util/PostCtrls' import {PostCtrls} from '../util/PostCtrls'
import {RichText} from '../util/RichText' import {RichText} from '../util/RichText'
import Toast from '../util/Toast' import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
@ -99,15 +99,11 @@ export const Post = observer(function Post({uri}: {uri: string}) {
item.delete().then( item.delete().then(
() => { () => {
setDeleted(true) setDeleted(true)
Toast.show('Post deleted', { Toast.show('Post deleted')
position: Toast.positions.TOP,
})
}, },
e => { e => {
console.error(e) console.error(e)
Toast.show('Failed to delete post, please try again', { Toast.show('Failed to delete post, please try again')
position: Toast.positions.TOP,
})
}, },
) )
} }
@ -196,7 +192,7 @@ const styles = StyleSheet.create({
}, },
postText: { postText: {
fontFamily: 'Helvetica Neue', fontFamily: 'Helvetica Neue',
fontSize: 17, fontSize: 16,
lineHeight: 22.1, // 1.3 of 17px lineHeight: 20.8, // 1.3 of 16px
}, },
}) })

View File

@ -11,7 +11,7 @@ import {PostMeta} from '../util/PostMeta'
import {PostCtrls} from '../util/PostCtrls' import {PostCtrls} from '../util/PostCtrls'
import {PostEmbeds} from '../util/PostEmbeds' import {PostEmbeds} from '../util/PostEmbeds'
import {RichText} from '../util/RichText' import {RichText} from '../util/RichText'
import Toast from '../util/Toast' import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
import {useStores} from '../../../state' import {useStores} from '../../../state'
@ -70,15 +70,11 @@ export const FeedItem = observer(function FeedItem({
item.delete().then( item.delete().then(
() => { () => {
setDeleted(true) setDeleted(true)
Toast.show('Post deleted', { Toast.show('Post deleted')
position: Toast.positions.TOP,
})
}, },
e => { e => {
console.error(e) console.error(e)
Toast.show('Failed to delete post, please try again', { Toast.show('Failed to delete post, please try again')
position: Toast.positions.TOP,
})
}, },
) )
} }
@ -254,7 +250,7 @@ const styles = StyleSheet.create({
}, },
postText: { postText: {
fontFamily: 'Helvetica Neue', fontFamily: 'Helvetica Neue',
fontSize: 17, fontSize: 16,
lineHeight: 22.1, // 1.3 of 17px lineHeight: 20.8, // 1.3 of 16px
}, },
}) })

View File

@ -1,12 +1,6 @@
import React, {useMemo} from 'react' import React, {useMemo} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import { import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
ActivityIndicator,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient' import LinearGradient from 'react-native-linear-gradient'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {AtUri} from '../../../third-party/uri' import {AtUri} from '../../../third-party/uri'
@ -20,9 +14,8 @@ import {
import {pluralize} from '../../../lib/strings' import {pluralize} from '../../../lib/strings'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
import {getGradient} from '../../lib/asset-gen' import {getGradient} from '../../lib/asset-gen'
import {MagnifyingGlassIcon} from '../../lib/icons'
import {DropdownBtn, DropdownItem} from '../util/DropdownBtn' import {DropdownBtn, DropdownItem} from '../util/DropdownBtn'
import Toast from '../util/Toast' import * as Toast from '../util/Toast'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder' import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {RichText} from '../util/RichText' import {RichText} from '../util/RichText'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
@ -55,10 +48,6 @@ export const ProfileHeader = observer(function ProfileHeader({
`${view.myState.follow ? 'Following' : 'No longer following'} ${ `${view.myState.follow ? 'Following' : 'No longer following'} ${
view.displayName || view.handle view.displayName || view.handle
}`, }`,
{
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
},
) )
}, },
err => console.error('Failed to toggle follow', err), err => console.error('Failed to toggle follow', err),
@ -94,10 +83,7 @@ export const ProfileHeader = observer(function ProfileHeader({
did: store.me.did || '', did: store.me.did || '',
rkey: new AtUri(view.myState.member).rkey, rkey: new AtUri(view.myState.member).rkey,
}) })
Toast.show(`Scene left`, { Toast.show(`Scene left`)
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
})
} }
onRefreshAll() onRefreshAll()
} }
@ -108,18 +94,6 @@ export const ProfileHeader = observer(function ProfileHeader({
return ( return (
<View style={styles.outer}> <View style={styles.outer}>
<LoadingPlaceholder width="100%" height={120} /> <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}> <View style={styles.avi}>
<LoadingPlaceholder <LoadingPlaceholder
width={80} width={80}
@ -179,18 +153,6 @@ export const ProfileHeader = observer(function ProfileHeader({
return ( return (
<View style={styles.outer}> <View style={styles.outer}>
<UserBanner handle={view.handle} /> <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}> <View style={styles.avi}>
<UserAvatar <UserAvatar
size={80} size={80}
@ -353,30 +315,6 @@ const styles = StyleSheet.create({
width: '100%', width: '100%',
height: 120, 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: { avi: {
position: 'absolute', position: 'absolute',
top: 80, top: 80,

View File

@ -12,9 +12,10 @@ import {UpIcon, UpIconSolid} from '../../lib/icons'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
interface PostCtrlsOpts { interface PostCtrlsOpts {
replyCount: number big?: boolean
repostCount: number replyCount?: number
upvoteCount: number repostCount?: number
upvoteCount?: number
isReposted: boolean isReposted: boolean
isUpvoted: boolean isUpvoted: boolean
onPressReply: () => void onPressReply: () => void
@ -30,17 +31,17 @@ export function PostCtrls(opts: PostCtrlsOpts) {
const interp2 = useSharedValue<number>(0) const interp2 = useSharedValue<number>(0)
const anim1Style = useAnimatedStyle(() => ({ 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]), opacity: interpolate(interp1.value, [0, 1.0], [1.0, 0.0]),
})) }))
const anim2Style = useAnimatedStyle(() => ({ 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]), opacity: interpolate(interp2.value, [0, 1.0], [1.0, 0.0]),
})) }))
const onPressToggleRepostWrapper = () => { const onPressToggleRepostWrapper = () => {
if (!opts.isReposted) { if (!opts.isReposted) {
interp1.value = withTiming(1, {duration: 300}, () => { interp1.value = withTiming(1, {duration: 400}, () => {
interp1.value = withDelay(100, withTiming(0, {duration: 20})) interp1.value = withDelay(100, withTiming(0, {duration: 20}))
}) })
} }
@ -48,7 +49,7 @@ export function PostCtrls(opts: PostCtrlsOpts) {
} }
const onPressToggleUpvoteWrapper = () => { const onPressToggleUpvoteWrapper = () => {
if (!opts.isUpvoted) { if (!opts.isUpvoted) {
interp2.value = withTiming(1, {duration: 300}, () => { interp2.value = withTiming(1, {duration: 400}, () => {
interp2.value = withDelay(100, withTiming(0, {duration: 20})) interp2.value = withDelay(100, withTiming(0, {duration: 20}))
}) })
} }
@ -62,9 +63,11 @@ export function PostCtrls(opts: PostCtrlsOpts) {
<FontAwesomeIcon <FontAwesomeIcon
style={styles.ctrlIcon} style={styles.ctrlIcon}
icon={['far', 'comment']} 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> </TouchableOpacity>
</View> </View>
<View style={s.flex1}> <View style={s.flex1}>
@ -77,17 +80,19 @@ export function PostCtrls(opts: PostCtrlsOpts) {
opts.isReposted ? styles.ctrlIconReposted : styles.ctrlIcon opts.isReposted ? styles.ctrlIconReposted : styles.ctrlIcon
} }
icon="retweet" icon="retweet"
size={18} size={opts.big ? 22 : 18}
/> />
</Animated.View> </Animated.View>
<Text {typeof opts.repostCount !== 'undefined' ? (
style={ <Text
opts.isReposted style={
? [s.bold, s.green3, s.f16, s.ml5] opts.isReposted
: [sRedgray, s.f16, s.ml5] ? [s.bold, s.green3, s.f16, s.ml5]
}> : [sRedgray, s.f16, s.ml5]
{opts.repostCount} }>
</Text> {opts.repostCount}
</Text>
) : undefined}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={s.flex1}> <View style={s.flex1}>
@ -96,19 +101,28 @@ export function PostCtrls(opts: PostCtrlsOpts) {
onPress={onPressToggleUpvoteWrapper}> onPress={onPressToggleUpvoteWrapper}>
<Animated.View style={anim2Style}> <Animated.View style={anim2Style}>
{opts.isUpvoted ? ( {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> </Animated.View>
<Text {typeof opts.upvoteCount !== 'undefined' ? (
style={ <Text
opts.isUpvoted style={
? [s.bold, s.red3, s.f16, s.ml5] opts.isUpvoted
: [sRedgray, s.f16, s.ml5] ? [s.bold, s.red3, s.f16, s.ml5]
}> : [sRedgray, s.f16, s.ml5]
{opts.upvoteCount} }>
</Text> {opts.upvoteCount}
</Text>
) : undefined}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={s.flex1}></View> <View style={s.flex1}></View>

View File

@ -1,2 +0,0 @@
import Toast from 'react-native-root-toast'
export default Toast

View File

@ -1,62 +1,11 @@
/* import Toast from 'react-native-root-toast'
* Note: the dataSet properties are used to leverage custom CSS in public/index.html
*/
import React, {useState, useEffect} from 'react' export function show(message: string) {
// @ts-ignore no declarations available -prf Toast.show(message, {
import {Text, View} from 'react-native-web' duration: Toast.durations.LONG,
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' position: 50,
shadow: true,
interface ActiveToast { animation: true,
text: string hideOnPress: true,
}
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)
}
}) })
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,
} }

View File

@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {UserAvatar} from './UserAvatar'
import {colors} from '../../lib/styles' import {colors} from '../../lib/styles'
import {MagnifyingGlassIcon} from '../../lib/icons' import {MagnifyingGlassIcon} from '../../lib/icons'
import {useStores} from '../../../state' import {useStores} from '../../../state'
@ -9,14 +8,19 @@ import {useStores} from '../../../state'
export function ViewHeader({ export function ViewHeader({
title, title,
subtitle, subtitle,
onPost,
}: { }: {
title: string title: string
subtitle?: string subtitle?: string
onPost?: () => void
}) { }) {
const store = useStores() const store = useStores()
const onPressBack = () => { const onPressBack = () => {
store.nav.tab.goBack() store.nav.tab.goBack()
} }
const onPressCompose = () => {
store.shell.openComposer({onPost})
}
const onPressSearch = () => { const onPressSearch = () => {
store.nav.navigate(`/search`) store.nav.navigate(`/search`)
} }
@ -26,9 +30,7 @@ export function ViewHeader({
<TouchableOpacity onPress={onPressBack} style={styles.backIcon}> <TouchableOpacity onPress={onPressBack} style={styles.backIcon}>
<FontAwesomeIcon size={18} icon="angle-left" style={{marginTop: 6}} /> <FontAwesomeIcon size={18} icon="angle-left" style={{marginTop: 6}} />
</TouchableOpacity> </TouchableOpacity>
) : ( ) : undefined}
<View style={styles.cornerPlaceholder} />
)}
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<Text style={styles.title}>{title}</Text> <Text style={styles.title}>{title}</Text>
{subtitle ? ( {subtitle ? (
@ -37,8 +39,17 @@ export function ViewHeader({
</Text> </Text>
) : undefined} ) : undefined}
</View> </View>
<TouchableOpacity onPress={onPressSearch} style={styles.searchBtn}> <TouchableOpacity onPress={onPressCompose} style={styles.btn}>
<MagnifyingGlassIcon size={17} style={styles.searchBtnIcon} /> <FontAwesomeIcon size={18} icon="plus" />
</TouchableOpacity>
<TouchableOpacity
onPress={onPressSearch}
style={[styles.btn, {marginLeft: 8}]}>
<MagnifyingGlassIcon
size={18}
strokeWidth={3}
style={styles.searchBtnIcon}
/>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
) )
@ -59,33 +70,28 @@ const styles = StyleSheet.create({
titleContainer: { titleContainer: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'baseline', alignItems: 'baseline',
marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
}, },
title: { title: {
fontSize: 16, fontSize: 21,
fontWeight: '600', fontWeight: '600',
}, },
subtitle: { subtitle: {
fontSize: 15, fontSize: 18,
marginLeft: 3, marginLeft: 6,
color: colors.gray4, color: colors.gray4,
maxWidth: 200, maxWidth: 200,
}, },
cornerPlaceholder: {
width: 30,
height: 30,
},
backIcon: {width: 30, height: 30}, backIcon: {width: 30, height: 30},
searchBtn: { btn: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: colors.gray1, backgroundColor: colors.gray1,
width: 30, width: 36,
height: 30, height: 36,
borderRadius: 15, borderRadius: 20,
}, },
searchBtnIcon: { searchBtnIcon: {
color: colors.black, color: colors.black,

View File

@ -94,15 +94,17 @@ export function HomeIconSolid({
export function MagnifyingGlassIcon({ export function MagnifyingGlassIcon({
style, style,
size, size,
strokeWidth = 2,
}: { }: {
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
size?: string | number size?: string | number
strokeWidth?: number
}) { }) {
return ( return (
<Svg <Svg
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
strokeWidth={2} strokeWidth={strokeWidth}
stroke="currentColor" stroke="currentColor"
width={size || 24} width={size || 24}
height={size || 24} height={size || 24}

View File

@ -47,6 +47,7 @@ export const Home = observer(function Home({
if (!visible) { if (!visible) {
return return
} }
if (hasSetup) { if (hasSetup) {
console.log('Updating home feed') console.log('Updating home feed')
defaultFeedView.update() defaultFeedView.update()
@ -80,7 +81,11 @@ export const Home = observer(function Home({
return ( return (
<View style={s.flex1}> <View style={s.flex1}>
<ViewHeader title="Bluesky" subtitle="Private Beta" /> <ViewHeader
title="Bluesky"
subtitle="Private Beta"
onPost={onCreatePost}
/>
<Feed <Feed
key="default" key="default"
feed={defaultFeedView} feed={defaultFeedView}
@ -106,8 +111,8 @@ const styles = StyleSheet.create({
left: 10, left: 10,
bottom: 15, bottom: 15,
backgroundColor: colors.pink3, backgroundColor: colors.pink3,
paddingHorizontal: 10, paddingHorizontal: 12,
paddingVertical: 8, paddingVertical: 10,
borderRadius: 30, borderRadius: 30,
shadowColor: '#000', shadowColor: '#000',
shadowOpacity: 0.3, shadowOpacity: 0.3,
@ -117,5 +122,6 @@ const styles = StyleSheet.create({
color: colors.white, color: colors.white,
fontWeight: 'bold', fontWeight: 'bold',
marginLeft: 5, marginLeft: 5,
fontSize: 16,
}, },
}) })

View File

@ -15,7 +15,8 @@ import {PostFeedLoadingPlaceholder} from '../com/util/LoadingPlaceholder'
import {ErrorScreen} from '../com/util/ErrorScreen' import {ErrorScreen} from '../com/util/ErrorScreen'
import {ErrorMessage} from '../com/util/ErrorMessage' import {ErrorMessage} from '../com/util/ErrorMessage'
import {EmptyState} from '../com/util/EmptyState' 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' import {s, colors} from '../lib/styles'
const LOADING_ITEM = {_reactKey: '__loading__'} 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.`, `You'll be able to invite them again if you change your mind.`,
async () => { async () => {
await uiState.members.removeMember(membership.did) await uiState.members.removeMember(membership.did)
Toast.show(`User removed`, { Toast.show(`User removed`)
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
})
}, },
), ),
) )
@ -219,8 +217,11 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
renderItem = () => <View /> renderItem = () => <View />
} }
const title =
uiState.profile.displayName || uiState.profile.handle || params.name
return ( return (
<View style={styles.container}> <View style={styles.container}>
<ViewHeader title={title} />
{uiState.profile.hasError ? ( {uiState.profile.hasError ? (
<ErrorScreen <ErrorScreen
title="Failed to load profile" title="Failed to load profile"

View File

@ -8,7 +8,6 @@ import {
TouchableWithoutFeedback, TouchableWithoutFeedback,
View, View,
} from 'react-native' } from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import Animated, { import Animated, {
useSharedValue, useSharedValue,
useAnimatedStyle, useAnimatedStyle,
@ -25,10 +24,17 @@ import {CreateSceneModel} from '../../../state/models/shell-ui'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
export const MainMenu = observer( export const MainMenu = observer(
({active, onClose}: {active: boolean; onClose: () => void}) => { ({
active,
insetBottom,
onClose,
}: {
active: boolean
insetBottom: number
onClose: () => void
}) => {
const store = useStores() const store = useStores()
const initInterp = useSharedValue<number>(0) const initInterp = useSharedValue<number>(0)
const insets = useSafeAreaInsets()
useEffect(() => { useEffect(() => {
if (active) { if (active) {
@ -172,7 +178,7 @@ export const MainMenu = observer(
<Animated.View <Animated.View
style={[ style={[
styles.wrapper, styles.wrapper,
{bottom: insets.bottom + 55}, {bottom: insetBottom + 45},
wrapperAnimStyle, wrapperAnimStyle,
]}> ]}>
<SafeAreaView> <SafeAreaView>
@ -267,7 +273,8 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
height: 40, height: 40,
paddingHorizontal: 10, paddingHorizontal: 10,
marginBottom: 16, marginTop: 12,
marginBottom: 20,
}, },
section: { section: {
paddingHorizontal: 10, paddingHorizontal: 10,

View File

@ -70,7 +70,7 @@ const Btn = ({
onPress?: (event: GestureResponderEvent) => void onPress?: (event: GestureResponderEvent) => void
onLongPress?: (event: GestureResponderEvent) => void onLongPress?: (event: GestureResponderEvent) => void
}) => { }) => {
let size = 21 let size = 24
let addedStyles let addedStyles
let IconEl let IconEl
if (icon === 'menu') { if (icon === 'menu') {
@ -79,17 +79,17 @@ const Btn = ({
IconEl = GridIconSolid IconEl = GridIconSolid
} else if (icon === 'home') { } else if (icon === 'home') {
IconEl = HomeIcon IconEl = HomeIcon
size = 24 size = 27
} else if (icon === 'home-solid') { } else if (icon === 'home-solid') {
IconEl = HomeIconSolid IconEl = HomeIconSolid
size = 24 size = 27
} else if (icon === 'bell') { } else if (icon === 'bell') {
IconEl = BellIcon IconEl = BellIcon
size = 24 size = 27
addedStyles = {position: 'relative', top: -1} as ViewStyle addedStyles = {position: 'relative', top: -1} as ViewStyle
} else if (icon === 'bell-solid') { } else if (icon === 'bell-solid') {
IconEl = BellIconSolid IconEl = BellIconSolid
size = 24 size = 27
addedStyles = {position: 'relative', top: -1} as ViewStyle addedStyles = {position: 'relative', top: -1} as ViewStyle
} else { } else {
IconEl = FontAwesomeIcon IconEl = FontAwesomeIcon
@ -316,7 +316,7 @@ export const MobileShell: React.FC = observer(() => {
<View <View
style={[ style={[
styles.bottomBar, styles.bottomBar,
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)}, {paddingBottom: clamp(safeAreaInsets.bottom, 15, 40)},
]}> ]}>
<Btn <Btn
icon={isAtHome ? 'home-solid' : 'home'} icon={isAtHome ? 'home-solid' : 'home'}
@ -343,6 +343,7 @@ export const MobileShell: React.FC = observer(() => {
</View> </View>
<MainMenu <MainMenu
active={isMainMenuActive} active={isMainMenuActive}
insetBottom={clamp(safeAreaInsets.bottom, 15, 40)}
onClose={() => setMainMenuActive(false)} onClose={() => setMainMenuActive(false)}
/> />
<Modal /> <Modal />
@ -491,7 +492,7 @@ const styles = StyleSheet.create({
}, },
ctrl: { ctrl: {
flex: 1, flex: 1,
paddingTop: 15, paddingTop: 12,
paddingBottom: 5, paddingBottom: 5,
}, },
notificationCount: { notificationCount: {

View File

@ -11718,6 +11718,11 @@ thunky@^1.0.2:
resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d"
integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== 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: tmpl@1.0.5:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"