Switch to autogenerated avis for now

zio/stable
Paul Frazee 2022-10-31 17:23:47 -05:00
parent eceef67d46
commit fd6a2b1b40
14 changed files with 186 additions and 77 deletions

View File

@ -6,7 +6,7 @@ import {FontAwesomeIcon, Props} from '@fortawesome/react-native-fontawesome'
import {NotificationsViewItemModel} from '../../../state/models/notifications-view' import {NotificationsViewItemModel} from '../../../state/models/notifications-view'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
import {ago, pluralize} from '../../lib/strings' import {ago, pluralize} from '../../lib/strings'
import {DEF_AVATER} from '../../lib/assets' import {UserAvatar} from '../util/UserAvatar'
import {PostText} from '../post/PostText' import {PostText} from '../post/PostText'
import {Post} from '../post/Post' import {Post} from '../post/Post'
import {Link} from '../util/Link' import {Link} from '../util/Link'
@ -114,7 +114,11 @@ export const FeedItem = observer(function FeedItem({
key={author.href} key={author.href}
href={author.href} href={author.href}
title={`@${author.name}`}> title={`@${author.name}`}>
<Image style={styles.avi} source={DEF_AVATER} /> <UserAvatar
size={30}
displayName={author.displayName}
name={author.name}
/>
</Link> </Link>
))} ))}
{authors.length > MAX_AUTHORS ? ( {authors.length > MAX_AUTHORS ? (
@ -197,12 +201,6 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
}, },
avi: {
width: 30,
height: 30,
borderRadius: 15,
resizeMode: 'cover',
},
aviExtraCount: { aviExtraCount: {
fontWeight: 'bold', fontWeight: 'bold',
paddingLeft: 6, paddingLeft: 6,

View File

@ -1,22 +1,14 @@
import React, {useState, useEffect} from 'react' import React, {useState, useEffect} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import { import {ActivityIndicator, FlatList, StyleSheet, Text, View} from 'react-native'
ActivityIndicator,
FlatList,
Image,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native'
import { import {
LikedByViewModel, LikedByViewModel,
LikedByViewItemModel, LikedByViewItemModel,
} from '../../../state/models/liked-by-view' } from '../../../state/models/liked-by-view'
import {Link} from '../util/Link' import {Link} from '../util/Link'
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'
import {DEF_AVATER} from '../../lib/assets'
export const PostLikedBy = observer(function PostLikedBy({uri}: {uri: string}) { export const PostLikedBy = observer(function PostLikedBy({uri}: {uri: string}) {
const store = useStores() const store = useStores()
@ -78,7 +70,11 @@ const LikedByItem = ({item}: {item: LikedByViewItemModel}) => {
<Link style={styles.outer} href={`/profile/${item.name}`} title={item.name}> <Link style={styles.outer} href={`/profile/${item.name}`} title={item.name}>
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<Image style={styles.avi} source={DEF_AVATER} /> <UserAvatar
size={40}
displayName={item.displayName}
name={item.name}
/>
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<Text style={[s.f15, s.bold]}>{item.displayName}</Text> <Text style={[s.f15, s.bold]}>{item.displayName}</Text>

View File

@ -12,10 +12,10 @@ import {
RepostedByViewModel, RepostedByViewModel,
RepostedByViewItemModel, RepostedByViewItemModel,
} from '../../../state/models/reposted-by-view' } from '../../../state/models/reposted-by-view'
import {UserAvatar} from '../util/UserAvatar'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
import {DEF_AVATER} from '../../lib/assets'
export const PostRepostedBy = observer(function PostRepostedBy({ export const PostRepostedBy = observer(function PostRepostedBy({
uri, uri,
@ -83,7 +83,11 @@ const RepostedByItem = ({item}: {item: RepostedByViewItemModel}) => {
<Link style={styles.outer} href={`/profile/${item.name}`} title={item.name}> <Link style={styles.outer} href={`/profile/${item.name}`} title={item.name}>
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<Image style={styles.avi} source={DEF_AVATER} /> <UserAvatar
size={40}
displayName={item.displayName}
name={item.name}
/>
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<Text style={[s.f15, s.bold]}>{item.displayName}</Text> <Text style={[s.f15, s.bold]}>{item.displayName}</Text>

View File

@ -10,9 +10,9 @@ import {ComposePostModel} from '../../../state/models/shell'
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 {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'
import {DEF_AVATER} from '../../lib/assets'
import {useStores} from '../../../state' import {useStores} from '../../../state'
const PARENT_REPLY_LINE_LENGTH = 8 const PARENT_REPLY_LINE_LENGTH = 8
@ -116,7 +116,11 @@ export const PostThreadItem = observer(function PostThreadItem({
<View style={styles.outer}> <View style={styles.outer}>
<View style={styles.layout}> <View style={styles.layout}>
<Link style={styles.layoutAvi} href={authorHref} title={authorTitle}> <Link style={styles.layoutAvi} href={authorHref} title={authorTitle}>
<Image style={styles.avi} source={DEF_AVATER} /> <UserAvatar
size={50}
displayName={item.author.displayName}
name={item.author.name}
/>
</Link> </Link>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<View style={[styles.meta, s.mt5]}> <View style={[styles.meta, s.mt5]}>
@ -231,7 +235,11 @@ export const PostThreadItem = observer(function PostThreadItem({
)} )}
<View style={styles.layout}> <View style={styles.layout}>
<Link style={styles.layoutAvi} href={authorHref} title={authorTitle}> <Link style={styles.layoutAvi} href={authorHref} title={authorTitle}>
<Image style={styles.avi} source={DEF_AVATER} /> <UserAvatar
size={50}
displayName={item.author.displayName}
name={item.author.name}
/>
</Link> </Link>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
{item.replyingToAuthor && {item.replyingToAuthor &&
@ -321,12 +329,6 @@ const styles = StyleSheet.create({
paddingTop: 10, paddingTop: 10,
paddingBottom: 10, paddingBottom: 10,
}, },
avi: {
width: 50,
height: 50,
borderRadius: 25,
resizeMode: 'cover',
},
layoutContent: { layoutContent: {
flex: 1, flex: 1,
paddingRight: 10, paddingRight: 10,

View File

@ -4,7 +4,6 @@ import {AtUri} from '../../../third-party/uri'
import * as PostType from '../../../third-party/api/src/types/app/bsky/post' import * as PostType from '../../../third-party/api/src/types/app/bsky/post'
import { import {
ActivityIndicator, ActivityIndicator,
Image,
StyleSheet, StyleSheet,
Text, Text,
TouchableOpacity, TouchableOpacity,
@ -16,10 +15,10 @@ import {ComposePostModel} from '../../../state/models/shell'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {UserInfoText} from '../util/UserInfoText' import {UserInfoText} from '../util/UserInfoText'
import {RichText} from '../util/RichText' import {RichText} from '../util/RichText'
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'
import {ago} from '../../lib/strings' import {ago} from '../../lib/strings'
import {DEF_AVATER} from '../../lib/assets'
export const Post = observer(function Post({uri}: {uri: string}) { export const Post = observer(function Post({uri}: {uri: string}) {
const store = useStores() const store = useStores()
@ -91,7 +90,11 @@ export const Post = observer(function Post({uri}: {uri: string}) {
<Link style={styles.outer} href={itemHref} title={itemTitle}> <Link style={styles.outer} href={itemHref} title={itemTitle}>
<View style={styles.layout}> <View style={styles.layout}>
<Link style={styles.layoutAvi} href={authorHref} title={authorTitle}> <Link style={styles.layoutAvi} href={authorHref} title={authorTitle}>
<Image style={styles.avi} source={DEF_AVATER} /> <UserAvatar
size={50}
displayName={item.author.displayName}
name={item.author.name}
/>
</Link> </Link>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<View style={styles.meta}> <View style={styles.meta}>
@ -185,12 +188,6 @@ const styles = StyleSheet.create({
layoutAvi: { layoutAvi: {
width: 60, width: 60,
}, },
avi: {
width: 50,
height: 50,
borderRadius: 25,
resizeMode: 'cover',
},
layoutContent: { layoutContent: {
flex: 1, flex: 1,
}, },

View File

@ -1,24 +1,24 @@
import React, {useMemo} from 'react' import React, {useMemo} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
import {AtUri} from '../../../third-party/uri' import {AtUri} from '../../../third-party/uri'
import * as PostType from '../../../third-party/api/src/types/app/bsky/post' import * as PostType from '../../../third-party/api/src/types/app/bsky/post'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FeedViewItemModel} from '../../../state/models/feed-view' import {FeedItemModel} from '../../../state/models/feed-view'
import {ComposePostModel, SharePostModel} from '../../../state/models/shell' import {ComposePostModel, SharePostModel} from '../../../state/models/shell'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {PostDropdownBtn} from '../util/DropdownBtn' import {PostDropdownBtn} from '../util/DropdownBtn'
import {UserInfoText} from '../util/UserInfoText' import {UserInfoText} from '../util/UserInfoText'
import {RichText} from '../util/RichText' import {RichText} from '../util/RichText'
import {UserAvatar} from '../util/UserAvatar'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
import {ago} from '../../lib/strings' import {ago} from '../../lib/strings'
import {DEF_AVATER} from '../../lib/assets'
import {useStores} from '../../../state' import {useStores} from '../../../state'
export const FeedItem = observer(function FeedItem({ export const FeedItem = observer(function FeedItem({
item, item,
}: { }: {
item: FeedViewItemModel item: FeedItemModel
}) { }) {
const store = useStores() const store = useStores()
const record = item.record as unknown as PostType.Record const record = item.record as unknown as PostType.Record
@ -73,7 +73,11 @@ export const FeedItem = observer(function FeedItem({
style={styles.layoutAvi} style={styles.layoutAvi}
href={authorHref} href={authorHref}
title={item.author.name}> title={item.author.name}>
<Image style={styles.avi} source={DEF_AVATER} /> <UserAvatar
size={50}
displayName={item.author.displayName}
name={item.author.name}
/>
</Link> </Link>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<View style={styles.meta}> <View style={styles.meta}>
@ -199,12 +203,6 @@ const styles = StyleSheet.create({
width: 60, width: 60,
paddingTop: 5, paddingTop: 5,
}, },
avi: {
width: 50,
height: 50,
borderRadius: 25,
resizeMode: 'cover',
},
layoutContent: { layoutContent: {
flex: 1, flex: 1,
}, },

View File

@ -13,9 +13,9 @@ import {
FollowerItem, FollowerItem,
} from '../../../state/models/user-followers-view' } from '../../../state/models/user-followers-view'
import {Link} from '../util/Link' import {Link} from '../util/Link'
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'
import {DEF_AVATER} from '../../lib/assets'
export const ProfileFollowers = observer(function ProfileFollowers({ export const ProfileFollowers = observer(function ProfileFollowers({
name, name,
@ -81,7 +81,11 @@ const User = ({item}: {item: FollowerItem}) => {
<Link style={styles.outer} href={`/profile/${item.name}`} title={item.name}> <Link style={styles.outer} href={`/profile/${item.name}`} title={item.name}>
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<Image style={styles.avi} source={DEF_AVATER} /> <UserAvatar
size={40}
displayName={item.displayName}
name={item.name}
/>
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<Text style={[s.f15, s.bold]}>{item.displayName}</Text> <Text style={[s.f15, s.bold]}>{item.displayName}</Text>
@ -106,12 +110,6 @@ const styles = StyleSheet.create({
paddingTop: 10, paddingTop: 10,
paddingBottom: 10, paddingBottom: 10,
}, },
avi: {
width: 40,
height: 40,
borderRadius: 20,
resizeMode: 'cover',
},
layoutContent: { layoutContent: {
flex: 1, flex: 1,
paddingRight: 10, paddingRight: 10,

View File

@ -14,8 +14,8 @@ import {
} from '../../../state/models/user-follows-view' } from '../../../state/models/user-follows-view'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {UserAvatar} from '../util/UserAvatar'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
import {DEF_AVATER} from '../../lib/assets'
export const ProfileFollows = observer(function ProfileFollows({ export const ProfileFollows = observer(function ProfileFollows({
name, name,
@ -81,7 +81,11 @@ const User = ({item}: {item: FollowItem}) => {
<Link style={styles.outer} href={`/profile/${item.name}`} title={item.name}> <Link style={styles.outer} href={`/profile/${item.name}`} title={item.name}>
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<Image style={styles.avi} source={DEF_AVATER} /> <UserAvatar
size={40}
displayName={item.displayName}
name={item.name}
/>
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<Text style={[s.f15, s.bold]}>{item.displayName}</Text> <Text style={[s.f15, s.bold]}>{item.displayName}</Text>

View File

@ -15,8 +15,9 @@ import {useStores} from '../../../state'
import {EditProfileModel} from '../../../state/models/shell' import {EditProfileModel} from '../../../state/models/shell'
import {pluralize} from '../../lib/strings' import {pluralize} from '../../lib/strings'
import {s, gradients, colors} from '../../lib/styles' import {s, gradients, colors} from '../../lib/styles'
import {DEF_AVATER, BANNER} from '../../lib/assets' import {BANNER} from '../../lib/assets'
import Toast from '../util/Toast' import Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar'
import {Link} from '../util/Link' import {Link} from '../util/Link'
export const ProfileHeader = observer(function ProfileHeader({ export const ProfileHeader = observer(function ProfileHeader({
@ -81,7 +82,9 @@ export const ProfileHeader = observer(function ProfileHeader({
return ( return (
<View style={styles.outer}> <View style={styles.outer}>
<Image style={styles.banner} source={BANNER} /> <Image style={styles.banner} source={BANNER} />
<Image style={styles.avi} source={DEF_AVATER} /> <View style={styles.avi}>
<UserAvatar size={80} displayName={view.displayName} name={view.name} />
</View>
<View style={styles.content}> <View style={styles.content}>
<View style={[styles.displayNameLine]}> <View style={[styles.displayNameLine]}>
<Text style={styles.displayName}>{view.displayName}</Text> <Text style={styles.displayName}>{view.displayName}</Text>
@ -178,12 +181,12 @@ const styles = StyleSheet.create({
position: 'absolute', position: 'absolute',
top: 80, top: 80,
left: 10, left: 10,
width: 80, width: 84,
height: 80, height: 84,
borderRadius: 40, borderRadius: 42,
resizeMode: 'cover',
borderWidth: 2, borderWidth: 2,
borderColor: colors.white, borderColor: colors.white,
backgroundColor: colors.white,
}, },
content: { content: {
paddingTop: 8, paddingTop: 8,

View File

@ -0,0 +1,78 @@
import React from 'react'
import Svg, {Circle, Text, Defs, LinearGradient, Stop} from 'react-native-svg'
import {colors} from '../../lib/styles'
const GRADIENTS = [
[colors.pink3, colors.purple3],
[colors.purple3, colors.blue3],
[colors.blue3, colors.green3],
[colors.red3, colors.pink3],
]
export function UserAvatar({
size,
displayName,
name,
}: {
size: number
displayName: string | undefined
name: string
}) {
const initials = getInitials(displayName || name)
const gi = cyrb53(name) % GRADIENTS.length
return (
<Svg width={size} height={size} viewBox="0 0 100 100">
<Defs>
<LinearGradient id="grad" x1="0" y1="0" x2="1" y2="1">
<Stop offset="0" stopColor={GRADIENTS[gi][0]} stopOpacity="1" />
<Stop offset="1" stopColor={GRADIENTS[gi][1]} stopOpacity="1" />
</LinearGradient>
</Defs>
<Circle cx="50" cy="50" r="50" fill="url(#grad)" />
<Text
fill="white"
fontSize="50"
fontWeight="bold"
x="50"
y="67"
textAnchor="middle">
{initials}
</Text>
</Svg>
)
}
function getInitials(str: string): string {
const tokens = str
.split(' ')
.filter(Boolean)
.map(v => v.trim())
if (tokens.length >= 2 && tokens[0][0] && tokens[0][1]) {
return tokens[0][0].toUpperCase() + tokens[1][0].toUpperCase()
}
if (tokens.length === 1 && tokens[0][0]) {
return tokens[0][0].toUpperCase()
}
return 'X'
}
// deterministic string->hash
// https://stackoverflow.com/a/52171480
function cyrb53(str: string, seed = 0): number {
let h1 = 0xdeadbeef ^ seed,
h2 = 0x41c6ce57 ^ seed
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i)
h1 = Math.imul(h1 ^ ch, 2654435761)
h2 = Math.imul(h2 ^ ch, 1597334677)
}
h1 =
Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
Math.imul(h2 ^ (h2 >>> 13), 3266489909)
h2 =
Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
Math.imul(h1 ^ (h1 >>> 13), 3266489909)
return 4294967296 * (2097151 & h2) + (h1 >>> 0)
}

View File

@ -4,8 +4,8 @@ import {observer} from 'mobx-react-lite'
import {useStores} from '../../state' import {useStores} from '../../state'
import {ScreenParams} from '../routes' import {ScreenParams} from '../routes'
import {s, colors} from '../lib/styles' import {s, colors} from '../lib/styles'
import {DEF_AVATER} from '../lib/assets'
import {Link} from '../com/util/Link' import {Link} from '../com/util/Link'
import {UserAvatar} from '../com/util/UserAvatar'
export const Settings = observer(function Settings({visible}: ScreenParams) { export const Settings = observer(function Settings({visible}: ScreenParams) {
const store = useStores() const store = useStores()
@ -33,8 +33,12 @@ export const Settings = observer(function Settings({visible}: ScreenParams) {
</View> </View>
<Link href={`/profile/${store.me.name}`} title="Your profile"> <Link href={`/profile/${store.me.name}`} title="Your profile">
<View style={styles.profile}> <View style={styles.profile}>
<Image style={styles.avi} source={DEF_AVATER} /> <UserAvatar
<View> size={40}
displayName={store.me.displayName}
name={store.me.name || ''}
/>
<View style={[s.ml10]}>
<Text style={[s.f18]}>{store.me.displayName}</Text> <Text style={[s.f18]}>{store.me.displayName}</Text>
<Text style={[s.gray5]}>@{store.me.name}</Text> <Text style={[s.gray5]}>@{store.me.name}</Text>
</View> </View>

View File

@ -19,6 +19,7 @@ import Animated, {
import {IconProp} from '@fortawesome/fontawesome-svg-core' import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {HomeIcon, UserGroupIcon} from '../../lib/icons' import {HomeIcon, UserGroupIcon} from '../../lib/icons'
import {UserAvatar} from '../../com/util/UserAvatar'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
import {DEF_AVATER} from '../../lib/assets' import {DEF_AVATER} from '../../lib/assets'
@ -131,7 +132,13 @@ export const MainMenu = observer(
<TouchableOpacity <TouchableOpacity
style={styles.profile} style={styles.profile}
onPress={() => onNavigate(`/profile/${store.me.name || ''}`)}> onPress={() => onNavigate(`/profile/${store.me.name || ''}`)}>
<Image style={styles.profileImage} source={DEF_AVATER} /> <View style={styles.profileImage}>
<UserAvatar
size={30}
displayName={store.me.displayName}
name={store.me.name || ''}
/>
</View>
<Text style={styles.profileText} numberOfLines={1}> <Text style={styles.profileText} numberOfLines={1}>
{store.me.displayName || store.me.name || 'My profile'} {store.me.displayName || store.me.name || 'My profile'}
</Text> </Text>
@ -231,9 +238,6 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
}, },
profileImage: { profileImage: {
borderRadius: 15,
width: 30,
height: 30,
marginRight: 8, marginRight: 8,
}, },
profileText: { profileText: {

View File

@ -21,7 +21,6 @@ import Swipeable from 'react-native-gesture-handler/Swipeable'
import LinearGradient from 'react-native-linear-gradient' import LinearGradient from 'react-native-linear-gradient'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {s, colors, gradients} from '../../lib/styles' import {s, colors, gradients} from '../../lib/styles'
import {DEF_AVATER} from '../../lib/assets'
import {match} from '../../routes' import {match} from '../../routes'
import {LinkActionsModel} from '../../../state/models/shell' import {LinkActionsModel} from '../../../state/models/shell'

View File

@ -4,11 +4,35 @@ Paul's todo list
- Update to RN 0.70 - Update to RN 0.70
- Cache some profile/userinfo lookups - Cache some profile/userinfo lookups
- Cursor behaviors on all views - Cursor behaviors on all views
- Home button should scroll to top - Update swipe behaviors: edge always goes back, leftmost always goes back, main connects to selector if present
- Onboarding flow - Onboarding flow
- * - *
- Private beta - Avatars
- Users list - SVG generate
- Main menu
- Scenes list
- Create scene view
- *
- Discover scenes view
- *
- User profile
- Distinguish by declared type
- User
- List scenes
- Invite to scene
- Remove from scene
- Scene
- Trending
- Members
- Profile header
- Invite to scene
- Remove from scene
- Edit profile
- Notifications
- Scene invite
- Reply gating
- Composer
- View on post
- Linking - Linking
- Web linking - Web linking
- App linking - App linking