Merge branch 'main' into upload-image
commit
c5f3200d6b
|
@ -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++) {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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]}>
|
|
||||||
· {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]}>
|
||||||
<></>
|
· {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,
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
import Toast from 'react-native-root-toast'
|
|
||||||
export default Toast
|
|
|
@ -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,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue