mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
update orbis comment and DM components
This commit is contained in:
parent
1cf6ded9ef
commit
1e0985cf65
887
package-lock.json
generated
887
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -29,7 +29,7 @@
|
||||
"@oceanprotocol/lib": "^2.4.0",
|
||||
"@oceanprotocol/typographies": "^0.1.0",
|
||||
"@oceanprotocol/use-dark-mode": "^2.4.3",
|
||||
"@orbisclub/orbis-sdk": "^0.3.60",
|
||||
"@orbisclub/orbis-sdk": "^0.4.4-beta.39",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@urql/exchange-refocus": "^1.0.0",
|
||||
"@walletconnect/web3-provider": "^1.8.0",
|
||||
|
@ -7,77 +7,191 @@ import React, {
|
||||
ReactElement
|
||||
} from 'react'
|
||||
import { sleep } from '@utils/index'
|
||||
import { useInterval } from '@hooks/useInterval'
|
||||
import { Orbis } from '@orbisclub/orbis-sdk'
|
||||
import { useWeb3 } from './Web3'
|
||||
import { accountTruncate } from '@utils/web3'
|
||||
import { didToAddress } from '@utils/orbis'
|
||||
|
||||
const OrbisContext = createContext(null)
|
||||
type IOrbisProvider = {
|
||||
orbis: IOrbis
|
||||
account: IOrbisProfile
|
||||
hasLit: boolean
|
||||
openConversations: boolean
|
||||
conversationId: string
|
||||
conversations: IOrbisConversation[]
|
||||
conversationTitle: string
|
||||
unreadMessages: IOrbisNotification[]
|
||||
connectOrbis: () => Promise<void>
|
||||
disconnectOrbis: () => void
|
||||
checkConnection: (value: boolean) => Promise<void>
|
||||
connectLit: () => Promise<{
|
||||
status?: number
|
||||
error?: any
|
||||
result?: string
|
||||
}>
|
||||
setOpenConversations: (value: boolean) => void
|
||||
setConversationId: (value: string) => void
|
||||
setConversations: (value: IOrbisConversation[]) => void
|
||||
getConversations: () => Promise<void>
|
||||
createConversation: (value: string) => Promise<void>
|
||||
checkConversation: (value: string) => IOrbisConversation[]
|
||||
getDid: (value: string) => Promise<string>
|
||||
}
|
||||
|
||||
const OrbisContext = createContext({} as IOrbisProvider)
|
||||
|
||||
const orbis: IOrbis = new Orbis()
|
||||
const NOTIFICATION_REFRESH_INTERVAL = 10000
|
||||
const CONVERSATION_CONTEXT = 'ocean_market'
|
||||
|
||||
function OrbisProvider({ children }: { children: ReactNode }): ReactElement {
|
||||
const { web3Provider } = useWeb3()
|
||||
const [orbis, setOrbis] = useState<OrbisInterface>()
|
||||
const [account, setAccount] = useState<OrbisAccountInterface>()
|
||||
const [convOpen, setConvOpen] = useState(false)
|
||||
const [account, setAccount] = useState<IOrbisProfile | null>(null)
|
||||
const [hasLit, setHasLit] = useState(false)
|
||||
const [openConversations, setOpenConversations] = useState(false)
|
||||
const [conversationId, setConversationId] = useState(null)
|
||||
const [conversations, setConversations] = useState([])
|
||||
const [conversationTitle, setConversationTitle] = useState(null)
|
||||
const [unreadMessages, setUnreadMessages] = useState<IOrbisNotification[]>([])
|
||||
|
||||
// Connecting to Orbis
|
||||
const connectOrbis = async (provider: object): Promise<void> => {
|
||||
if (!orbis) return
|
||||
|
||||
const res = await orbis.connect(provider)
|
||||
if (res.status !== 200) {
|
||||
await sleep(2000)
|
||||
connectOrbis(provider)
|
||||
const connectOrbis = async () => {
|
||||
const res = await orbis.connect_v2({
|
||||
provider: web3Provider,
|
||||
chain: 'ethereum'
|
||||
})
|
||||
if (res.status === 200) {
|
||||
const { data } = await orbis.getProfile(res.did)
|
||||
setAccount(data)
|
||||
} else {
|
||||
setAccount(res)
|
||||
await sleep(2000)
|
||||
await connectOrbis()
|
||||
}
|
||||
}
|
||||
|
||||
const checkConnection = async (): Promise<void> => {
|
||||
const disconnectOrbis = () => {
|
||||
const res = orbis.logout()
|
||||
if (res.status === 200) {
|
||||
setAccount(null)
|
||||
}
|
||||
}
|
||||
|
||||
const connectLit = async () => {
|
||||
const res = await orbis.connectLit(web3Provider)
|
||||
setHasLit(res.status === 200)
|
||||
return res
|
||||
}
|
||||
|
||||
const checkConnection = async (autoConnect = true) => {
|
||||
const res = await orbis.isConnected()
|
||||
|
||||
if (res.status === 200) {
|
||||
setAccount(res)
|
||||
} else {
|
||||
connectOrbis(web3Provider)
|
||||
setHasLit(res.details.hasLit)
|
||||
const { data } = await orbis.getProfile(res.did)
|
||||
setAccount(data)
|
||||
} else if (autoConnect) {
|
||||
await connectOrbis()
|
||||
}
|
||||
}
|
||||
|
||||
// Init Orbis
|
||||
useEffect(() => {
|
||||
const _orbis = new Orbis()
|
||||
setOrbis(_orbis)
|
||||
}, [])
|
||||
|
||||
const getConversations = async () => {
|
||||
const { data, error } = await orbis.getConversations({
|
||||
did: account?.did,
|
||||
context: 'ocean_market'
|
||||
context: CONVERSATION_CONTEXT
|
||||
})
|
||||
|
||||
if (data) {
|
||||
console.log(data)
|
||||
setConversations(data)
|
||||
}
|
||||
if (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
if (data) {
|
||||
setConversations(data)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Check if wallet connected
|
||||
if (!account && orbis && web3Provider) {
|
||||
checkConnection()
|
||||
const checkConversation = (userDid: string) => {
|
||||
const filtered: IOrbisConversation[] = conversations.filter(
|
||||
(conversation: IOrbisConversation) => {
|
||||
return conversation.recipients.includes(userDid)
|
||||
}
|
||||
)
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
const createConversation = async (userDid: string) => {
|
||||
if (!hasLit) {
|
||||
const res = await connectLit()
|
||||
if (res.status !== 200) return
|
||||
}
|
||||
|
||||
// Fetch conversations
|
||||
if (account && orbis) {
|
||||
getConversations()
|
||||
const convoExists = checkConversation(userDid)
|
||||
|
||||
if (convoExists.length > 0) {
|
||||
setConversationId(convoExists[0].stream_id)
|
||||
setOpenConversations(true)
|
||||
} else {
|
||||
const res = await orbis.createConversation({
|
||||
recipients: [userDid],
|
||||
context: CONVERSATION_CONTEXT
|
||||
})
|
||||
if (res.status === 200) {
|
||||
setConversationId(res.doc)
|
||||
setOpenConversations(true)
|
||||
}
|
||||
}
|
||||
}, [account, orbis, web3Provider])
|
||||
}
|
||||
|
||||
const getMessageNotifs = async () => {
|
||||
const { data, error } = await orbis.api.rpc('orbis_f_notifications', {
|
||||
user_did: account?.did || 'none',
|
||||
notif_type: 'messages'
|
||||
})
|
||||
|
||||
if (error) {
|
||||
console.log(error)
|
||||
setUnreadMessages([])
|
||||
} else if (data.length > 0) {
|
||||
const _unreads = data.filter((o: IOrbisNotification) => {
|
||||
return o.status === 'new'
|
||||
})
|
||||
setUnreadMessages(_unreads)
|
||||
}
|
||||
}
|
||||
|
||||
const getDid = async (address: string) => {
|
||||
if (!address) return null
|
||||
|
||||
const { data, error } = await orbis.getDids(address)
|
||||
|
||||
if (error) {
|
||||
console.log(error)
|
||||
return
|
||||
}
|
||||
|
||||
let _did: string = null
|
||||
|
||||
if (data && data.length > 0) {
|
||||
_did = data[0].did
|
||||
}
|
||||
|
||||
return _did
|
||||
}
|
||||
|
||||
useInterval(async () => {
|
||||
await getMessageNotifs()
|
||||
}, NOTIFICATION_REFRESH_INTERVAL)
|
||||
|
||||
useEffect(() => {
|
||||
if (account) {
|
||||
// Fetch conversations
|
||||
getConversations()
|
||||
} else if (web3Provider) {
|
||||
// Check if wallet connected
|
||||
checkConnection()
|
||||
}
|
||||
}, [account, web3Provider])
|
||||
|
||||
useEffect(() => {
|
||||
if (conversationId && conversations.length) {
|
||||
@ -86,7 +200,7 @@ function OrbisProvider({ children }: { children: ReactNode }): ReactElement {
|
||||
)
|
||||
if (conversation) {
|
||||
const recipient = conversation.recipients_details.find(
|
||||
(o: any) => o.did !== account.did
|
||||
(o: IOrbisProfile) => o.did !== account.did
|
||||
)
|
||||
|
||||
const address =
|
||||
@ -108,15 +222,23 @@ function OrbisProvider({ children }: { children: ReactNode }): ReactElement {
|
||||
value={{
|
||||
orbis,
|
||||
account,
|
||||
connectOrbis,
|
||||
setConvOpen,
|
||||
convOpen,
|
||||
hasLit,
|
||||
openConversations,
|
||||
conversationId,
|
||||
setConversationId,
|
||||
conversations,
|
||||
conversationTitle,
|
||||
unreadMessages,
|
||||
connectOrbis,
|
||||
disconnectOrbis,
|
||||
checkConnection,
|
||||
connectLit,
|
||||
setOpenConversations,
|
||||
setConversationId,
|
||||
setConversations,
|
||||
getConversations,
|
||||
conversationTitle
|
||||
createConversation,
|
||||
checkConversation,
|
||||
getDid
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
25
src/@hooks/useInterval.ts
Normal file
25
src/@hooks/useInterval.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export const useInterval = (
|
||||
callback: () => Promise<void>,
|
||||
delay: number | null | false
|
||||
) => {
|
||||
const savedCallback = useRef<() => Promise<void>>()
|
||||
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback
|
||||
}, [callback])
|
||||
|
||||
useEffect(() => {
|
||||
const tick = () => {
|
||||
savedCallback?.current()
|
||||
}
|
||||
|
||||
if (delay) {
|
||||
const id = setInterval(tick, delay)
|
||||
return () => {
|
||||
clearInterval(id)
|
||||
}
|
||||
}
|
||||
}, [callback, delay])
|
||||
}
|
Before Width: | Height: | Size: 232 B After Width: | Height: | Size: 232 B |
1
src/@images/ellipsis.svg
Normal file
1
src/@images/ellipsis.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="256" cy="256" r="48"></circle><circle cx="416" cy="256" r="48"></circle><circle cx="96" cy="256" r="48"></circle></svg>
|
After Width: | Height: | Size: 278 B |
1
src/@images/reply.svg
Normal file
1
src/@images/reply.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg stroke-width="0" viewBox="0 0 16 16" width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M6.598 5.013a.144.144 0 0 1 .202.134V6.3a.5.5 0 0 0 .5.5c.667 0 2.013.005 3.3.822.984.624 1.99 1.76 2.595 3.876-1.02-.983-2.185-1.516-3.205-1.799a8.74 8.74 0 0 0-1.921-.306 7.404 7.404 0 0 0-.798.008h-.013l-.005.001h-.001L7.3 9.9l-.05-.498a.5.5 0 0 0-.45.498v1.153c0 .108-.11.176-.202.134L2.614 8.254a.503.503 0 0 0-.042-.028.147.147 0 0 1 0-.252.499.499 0 0 0 .042-.028l3.984-2.933zM7.8 10.386c.068 0 .143.003.223.006.434.02 1.034.086 1.7.271 1.326.368 2.896 1.202 3.94 3.08a.5.5 0 0 0 .933-.305c-.464-3.71-1.886-5.662-3.46-6.66-1.245-.79-2.527-.942-3.336-.971v-.66a1.144 1.144 0 0 0-1.767-.96l-3.994 2.94a1.147 1.147 0 0 0 0 1.946l3.994 2.94a1.144 1.144 0 0 0 1.767-.96v-.667z"></path></svg>
|
After Width: | Height: | Size: 803 B |
626
src/@types/Orbis.d.ts
vendored
626
src/@types/Orbis.d.ts
vendored
@ -1,125 +1,478 @@
|
||||
// import { CeramicClient } from '@ceramicnetwork/http-client'
|
||||
// import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
|
||||
declare module '@orbisclub/orbis-sdk'
|
||||
|
||||
declare interface OrbisInterface {
|
||||
connect: function
|
||||
connectLit: function
|
||||
connectWithSeed: function
|
||||
createChannel: function
|
||||
createContext: function
|
||||
createConversation: function
|
||||
createGroup: function
|
||||
createPost: (options: any) => void
|
||||
createTileDocument: function
|
||||
decryptMessage: function
|
||||
decryptPost: function
|
||||
deletePost: function
|
||||
deterministicDocument: function
|
||||
editPost: function
|
||||
getChannel: function
|
||||
getConversation: function
|
||||
getConversations: function
|
||||
getDids: function
|
||||
getGroup: function
|
||||
getGroupMembers: function
|
||||
getGroups: function
|
||||
getIsFollowing: function
|
||||
getIsGroupMember: function
|
||||
getMessages: function
|
||||
getNotifications: function
|
||||
getPost: function
|
||||
getPosts: function
|
||||
getProfile: function
|
||||
getProfileFollowers: function
|
||||
getProfileFollowing: function
|
||||
getProfileGroups: function
|
||||
getProfilesByUsername: function
|
||||
isConnected: function
|
||||
logout: function
|
||||
react: function
|
||||
sendMessage: function
|
||||
setFollow: function
|
||||
setGroupMember: function
|
||||
setNotificationsReadTime: function
|
||||
testConnectSolana: function
|
||||
updateChannel: function
|
||||
updateContext: function
|
||||
updateGroup: function
|
||||
updatePost: function
|
||||
updateProfile: function
|
||||
updateTileDocument: function
|
||||
declare interface IOrbisConstructor {
|
||||
ceramic?: any
|
||||
node?: any
|
||||
store?: string
|
||||
PINATA_GATEWAY?: string
|
||||
PINATA_API_KEY?: string
|
||||
PINATA_SECRET_API_KEY?: string
|
||||
useLit?: boolean
|
||||
}
|
||||
|
||||
declare interface OrbisAccountInterface {
|
||||
details: object
|
||||
did: string
|
||||
result: string
|
||||
status: number
|
||||
}
|
||||
|
||||
declare interface OrbisPostCreatorDetailsInterface {
|
||||
a_r: number
|
||||
did: string
|
||||
metadata: {
|
||||
address: string
|
||||
chain: string
|
||||
ensName: string
|
||||
declare interface IOrbis {
|
||||
api: any
|
||||
connect: (provider: any, lit?: boolean) => Promise<IOrbisConnectReturns>
|
||||
connect_v2: (opts?: {
|
||||
provider?: any
|
||||
chain?: string
|
||||
lit?: boolean
|
||||
oauth?: any
|
||||
}) => Promise<IOrbisConnectReturns>
|
||||
connectLit: (provider: any) => Promise<{
|
||||
status?: number
|
||||
error?: any
|
||||
result?: string
|
||||
}>
|
||||
connectWithSeed: (seed: Uint8Array) => Promise<IOrbisConnectReturns>
|
||||
createChannel: (
|
||||
group_id: string,
|
||||
content: Pick<IOrbisChannel, 'content'>
|
||||
) => Promise<{
|
||||
status: number
|
||||
doc: string
|
||||
result: string
|
||||
}>
|
||||
createContext: () => void
|
||||
createConversation: (content: {
|
||||
recipients: string[]
|
||||
name?: string
|
||||
description?: string
|
||||
context?: string
|
||||
}) => Promise<{
|
||||
status: number
|
||||
doc: string
|
||||
result: string
|
||||
}>
|
||||
createGroup: (content: {
|
||||
name: string
|
||||
pfp?: string
|
||||
description?: string
|
||||
}) => Promise<{
|
||||
status: number
|
||||
doc: string
|
||||
result: string
|
||||
}>
|
||||
createPost: (content: IOrbisPostContent) => Promise<{
|
||||
status: number
|
||||
doc: string
|
||||
result: string
|
||||
}>
|
||||
createTileDocument: (
|
||||
content: any,
|
||||
tags: string[],
|
||||
schema: string,
|
||||
family: string
|
||||
) => Promise<{
|
||||
status: number
|
||||
doc?: string
|
||||
error?: any
|
||||
result: string
|
||||
}>
|
||||
decryptMessage: (content: {
|
||||
conversation_id: string
|
||||
encryptedMessage: IOrbisEncryptedBody
|
||||
}) => Promise<any>
|
||||
decryptPost: (content: IOrbisPostContent) => Promise<any>
|
||||
deletePost: (stream_id: string) => Promise<{
|
||||
status: number
|
||||
result: string
|
||||
}>
|
||||
deterministicDocument: (
|
||||
content: any,
|
||||
tags: string[],
|
||||
schema?: string,
|
||||
family?: string
|
||||
) => Promise<{
|
||||
status: number
|
||||
doc?: string
|
||||
error?: any
|
||||
result: string
|
||||
}>
|
||||
editPost: (
|
||||
stream_id: string,
|
||||
content: IOrbisPostContent,
|
||||
encryptionRules?: IOrbisEncryptionRules
|
||||
) => Promise<{
|
||||
status: number
|
||||
result: string
|
||||
}>
|
||||
getChannel: (channel_id: string) => Promise<{
|
||||
data: IOrbisChannel
|
||||
error: any
|
||||
status: number
|
||||
}>
|
||||
getConversation: (conversation_id: string) => Promise<{
|
||||
status: number
|
||||
data: any
|
||||
error: any
|
||||
}>
|
||||
getConversations: (opts: { did: string; context?: string }) => Promise<any>
|
||||
getDids: (address: string) => Promise<{
|
||||
data: any
|
||||
error: any
|
||||
status: number
|
||||
}>
|
||||
getGroup: (group_id: string) => Promise<{
|
||||
data: IOrbisGroup
|
||||
error: any
|
||||
status: any
|
||||
}>
|
||||
getGroupMembers: (group_id: string) => Promise<{
|
||||
data: {
|
||||
active: 'true' | 'false'
|
||||
content: {
|
||||
active: boolean
|
||||
group_id: string
|
||||
}
|
||||
created_at: string
|
||||
did: string
|
||||
group_id: string
|
||||
profile_details: Pick<IOrbisProfile['details'], 'did' | 'profile'>
|
||||
stream_id: string
|
||||
timestamp: number
|
||||
}[]
|
||||
error: any
|
||||
status: number
|
||||
}>
|
||||
getGroups: () => Promise<{
|
||||
data: IOrbisGroup[]
|
||||
error: any
|
||||
status: number
|
||||
}>
|
||||
getIsFollowing: (
|
||||
did_following: string,
|
||||
did_followed: string
|
||||
) => Promise<{
|
||||
data: boolean
|
||||
error: any
|
||||
status: number
|
||||
}>
|
||||
getIsGroupMember: (
|
||||
group_id: string,
|
||||
did: string
|
||||
) => Promise<{
|
||||
data: boolean
|
||||
error: any
|
||||
status: number
|
||||
}>
|
||||
getMessages: (
|
||||
conversation_id: string,
|
||||
page: number
|
||||
) => Promise<{
|
||||
data: IOrbisMessage[]
|
||||
error: any
|
||||
status: number
|
||||
}>
|
||||
getNotifications: (options: { type: string; context?: string }) => Promise<{
|
||||
data: any
|
||||
error: any
|
||||
status: number
|
||||
}>
|
||||
getPost: (post_id: string) => Promise<{
|
||||
data: IOrbisPost
|
||||
error: any
|
||||
status: number
|
||||
}>
|
||||
getPosts: (
|
||||
options: {
|
||||
context?: string
|
||||
did?: string
|
||||
master?: string
|
||||
only_master?: boolean
|
||||
tag?: string
|
||||
algorithm?: keyof typeof IOrbisGetPostsAlgorithm | null
|
||||
},
|
||||
page: number
|
||||
) => Promise<{
|
||||
data: IOrbisPost[]
|
||||
error: any
|
||||
status: number
|
||||
}>
|
||||
getReaction: (
|
||||
post_id: string,
|
||||
did: string
|
||||
) => Promise<{
|
||||
data: { type: string }
|
||||
error: any
|
||||
status: number
|
||||
}>
|
||||
getProfile: (did: string) => Promise<{
|
||||
data: IOrbisProfile
|
||||
error: any
|
||||
status: number
|
||||
}>
|
||||
getProfileFollowers: (did: string) => Promise<{
|
||||
data: IOrbisProfile['details'][]
|
||||
error: any
|
||||
status: number
|
||||
}>
|
||||
getProfileFollowing: (did: string) => Promise<{
|
||||
data: IOrbisProfile['details'][]
|
||||
error: any
|
||||
status: number
|
||||
}>
|
||||
getProfileGroups: (did: string) => Promise<{
|
||||
data: {
|
||||
active: 'true' | 'false'
|
||||
content: {
|
||||
active: boolean
|
||||
group_id: string
|
||||
}
|
||||
created_at: string
|
||||
did: string
|
||||
group_details: IOrbisGroup['content']
|
||||
group_id: string
|
||||
stream_id: string
|
||||
}[]
|
||||
error: any
|
||||
status: number
|
||||
}>
|
||||
getProfilesByUsername: (username: string) => Promise<{
|
||||
data: IOrbisProfile[]
|
||||
error: any
|
||||
status: number
|
||||
}>
|
||||
isConnected: (sessionString?: string) => Promise<IOrbisConnectReturns>
|
||||
logout: () => {
|
||||
status: number
|
||||
result: string
|
||||
error: any
|
||||
}
|
||||
nonces?: object
|
||||
profile?: OrbisCreatorProfileInterface
|
||||
react: (
|
||||
post_id: string,
|
||||
type: string
|
||||
) => Promise<{
|
||||
status: number
|
||||
doc: string
|
||||
result: string
|
||||
}>
|
||||
sendMessage: (content: { conversation_id: string; body: string }) => Promise<{
|
||||
status: number
|
||||
doc: string
|
||||
result: string
|
||||
}>
|
||||
setFollow: (
|
||||
did: string,
|
||||
active: boolean
|
||||
) => Promise<{
|
||||
status: number
|
||||
doc: string
|
||||
result: string
|
||||
}>
|
||||
setGroupMember: (
|
||||
group_id: string,
|
||||
active: boolean
|
||||
) => Promise<{
|
||||
status: number
|
||||
doc?: string
|
||||
error?: any
|
||||
result: string
|
||||
}>
|
||||
setNotificationsReadTime: (
|
||||
type: string,
|
||||
timestamp: number,
|
||||
context?: string
|
||||
) => Promise<{
|
||||
status: number
|
||||
doc?: string
|
||||
error?: any
|
||||
result: string
|
||||
}>
|
||||
updateChannel: (
|
||||
channel_id: string,
|
||||
content: Pick<IOrbisChannel, 'content'>
|
||||
) => Promise<{
|
||||
status: number
|
||||
doc: string
|
||||
result: string
|
||||
}>
|
||||
updateContext: () => void
|
||||
updateGroup: (
|
||||
stream_id: string,
|
||||
content: { pfp: string; name: string; description: string }
|
||||
) => Promise<{
|
||||
status: number
|
||||
doc: string
|
||||
result: string
|
||||
}>
|
||||
updatePost: (stream_id: string, body: string) => void
|
||||
updateProfile: (content: {
|
||||
pfp: string
|
||||
cover: string
|
||||
username: string
|
||||
description: string
|
||||
pfpIsNft: {
|
||||
chain: string
|
||||
contract: string
|
||||
tokenId: string
|
||||
timestamp: string
|
||||
}
|
||||
data?: object
|
||||
}) => Promise<{
|
||||
status: number
|
||||
doc: string
|
||||
result: string
|
||||
}>
|
||||
updateTileDocument: (
|
||||
stream_id: string,
|
||||
content: any,
|
||||
tags: string[],
|
||||
schema: string,
|
||||
family?: string
|
||||
) => Promise<{
|
||||
status: number
|
||||
doc?: string
|
||||
error?: any
|
||||
result: string
|
||||
}>
|
||||
uploadMedia: (file: File) => Promise<{
|
||||
status: number
|
||||
error?: any
|
||||
result: any
|
||||
}>
|
||||
}
|
||||
|
||||
declare interface OrbisPostMentionsInterface {
|
||||
interface IOrbisConnectReturns {
|
||||
status: number
|
||||
did: string
|
||||
details: any
|
||||
result: string
|
||||
}
|
||||
|
||||
declare enum IOrbisGetPostsAlgorithm {
|
||||
'recommendations',
|
||||
'all-posts',
|
||||
'all-master-posts',
|
||||
'all-did-master-posts',
|
||||
'all-context-master-posts',
|
||||
'all-posts-non-filtered',
|
||||
''
|
||||
}
|
||||
|
||||
declare enum OrbisReaction {
|
||||
'like',
|
||||
'haha',
|
||||
'downvote'
|
||||
}
|
||||
|
||||
interface IOrbisGroup {
|
||||
channels: Pick<IOrbisChannel, 'content' | 'stream_id'>[]
|
||||
content: {
|
||||
name?: string
|
||||
pfp?: string
|
||||
description?: string
|
||||
}
|
||||
count_members: number
|
||||
creator: string
|
||||
last_activity_timestamp: number
|
||||
stream_id: string
|
||||
}
|
||||
|
||||
interface IOrbisChannel {
|
||||
archived?: boolean
|
||||
content: {
|
||||
group_id: string
|
||||
name: string
|
||||
description: string
|
||||
type: 'chat' | 'feed'
|
||||
encryptionRules?: {
|
||||
type: string
|
||||
chain: string
|
||||
contractType: 'ERC20' | 'ERC721' | 'ERC1155'
|
||||
contractAddress: string
|
||||
minTokenBalance: string
|
||||
tokenId?: string
|
||||
}
|
||||
data?: object
|
||||
}
|
||||
created_at: string
|
||||
creator: string
|
||||
group_id: string
|
||||
stream_id: string
|
||||
timestamp: number
|
||||
type: 'chat' | 'feed'
|
||||
}
|
||||
|
||||
declare interface IOrbisProfile {
|
||||
address: string
|
||||
count_followers: number
|
||||
count_following: number
|
||||
details: {
|
||||
a_r?: number
|
||||
did: string
|
||||
metadata?: {
|
||||
address?: string
|
||||
chain?: string
|
||||
ensName?: string
|
||||
}
|
||||
nonces?: any
|
||||
profile?: {
|
||||
cover?: string
|
||||
data?: object
|
||||
description?: string
|
||||
pfp?: string
|
||||
pfpIsNft?: {
|
||||
chain: string
|
||||
contract: string
|
||||
timestamp: string
|
||||
tokenId: string
|
||||
}
|
||||
username?: string
|
||||
}
|
||||
twitter_details?: any
|
||||
}
|
||||
did: string
|
||||
last_activity_timestamp: number
|
||||
username: string
|
||||
}
|
||||
|
||||
interface IOrbisEncryptionRules {
|
||||
type: 'token-gated' | 'custom'
|
||||
chain: string
|
||||
contractType: 'ERC20' | 'ERC721' | 'ERC1155'
|
||||
contractAddress: string
|
||||
minTokenBalance: string
|
||||
tokenId: string
|
||||
accessControlConditions?: object
|
||||
}
|
||||
|
||||
interface IOrbisEncryptedBody {
|
||||
accessControlConditions: string
|
||||
encryptedString: string
|
||||
encryptedSymmetricKey: string
|
||||
}
|
||||
|
||||
interface IOrbisPostMention {
|
||||
did: string
|
||||
username: string
|
||||
}
|
||||
|
||||
interface OrbisPostContentInterface {
|
||||
interface IOrbisPostContent {
|
||||
body: string
|
||||
title?: string
|
||||
context?: string
|
||||
master?: string
|
||||
mentions?: OrbisPostMentionsInterface[]
|
||||
mentions?: IOrbisPostMention[]
|
||||
reply_to?: string
|
||||
type?: string
|
||||
encryptedMessage?: object
|
||||
tags?: {
|
||||
slug: string
|
||||
title: string
|
||||
}[]
|
||||
data?: object
|
||||
encryptionRules?: IOrbisEncryptionRules | null
|
||||
encryptedMessage?: object | null
|
||||
encryptedBody?: IOrbisEncryptedBody | null
|
||||
}
|
||||
|
||||
interface OrbisCreatorMetadataInterface {
|
||||
address?: string
|
||||
chain?: string
|
||||
ensName?: string
|
||||
}
|
||||
|
||||
interface OrbisCreatorProfileInterface {
|
||||
description?: string
|
||||
pfp?: string
|
||||
pfpIsNft?: {
|
||||
chain: string
|
||||
contract: string
|
||||
timestamp: string
|
||||
tokenId: string
|
||||
}
|
||||
username?: string
|
||||
}
|
||||
|
||||
declare interface OrbisPostInterface {
|
||||
content: OrbisPostContentInterface
|
||||
declare interface IOrbisPost {
|
||||
content: IOrbisPostContent
|
||||
context?: string
|
||||
context_details?: {
|
||||
channel_details?: {
|
||||
description: string
|
||||
group_id: string
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
channel_details?: IOrbisChannel['content']
|
||||
channel_id?: string
|
||||
group_details?: {
|
||||
description: string
|
||||
name: string
|
||||
pfp: string
|
||||
}
|
||||
group_details?: IOrbisGroup['content']
|
||||
group_id?: string
|
||||
}
|
||||
count_commits?: number
|
||||
@ -128,28 +481,59 @@ declare interface OrbisPostInterface {
|
||||
count_likes?: number
|
||||
count_replies?: number
|
||||
creator: string
|
||||
creator_details?: {
|
||||
a_r?: number
|
||||
did: string
|
||||
metadata?: OrbisCreatorMetadataInterface
|
||||
nonces?: object
|
||||
profile?: OrbisCreatorProfileInterface
|
||||
}
|
||||
creator_details?: IOrbisProfile['details']
|
||||
group_id?: string | null
|
||||
indexing_metadata?: {
|
||||
language?: string
|
||||
urlMetadata?: Record<string, string>
|
||||
}
|
||||
master?: string | null
|
||||
reply_to?: string | null
|
||||
reply_to_creator_details?: {
|
||||
did: string
|
||||
metadata: OrbisCreatorMetadataInterface
|
||||
profile: OrbisCreatorProfileInterface
|
||||
}
|
||||
reply_to_details?: OrbisPostContentInterface
|
||||
reply_to_creator_details?: Pick<
|
||||
IOrbisProfile['details'],
|
||||
'did' | 'metadata' | 'profile'
|
||||
>
|
||||
reply_to_details?: IOrbisPostContent
|
||||
stream_id: string
|
||||
timestamp: number
|
||||
type?: string
|
||||
}
|
||||
|
||||
declare interface OrbisConversationInterface {
|
||||
declare interface IOrbisMessageContent {
|
||||
conversation_id?: string
|
||||
encryptedMessage?: {
|
||||
accessControlConditions: string
|
||||
encryptedString: string
|
||||
encryptedSymmetricKey: string
|
||||
} | null
|
||||
encryptedMessageSolana?: {
|
||||
accessControlConditions: string
|
||||
encryptedString: string
|
||||
encryptedSymmetricKey: string
|
||||
} | null
|
||||
master?: string | null
|
||||
reply_to?: string | null
|
||||
}
|
||||
|
||||
declare interface IOrbisMessage {
|
||||
content: IOrbisMessageContent
|
||||
conversation_id: string
|
||||
created_at?: string
|
||||
creator: string
|
||||
creator_details: IOrbisProfile['details']
|
||||
master?: string | null
|
||||
recipients?: string[]
|
||||
reply_to?: string | null
|
||||
reply_to_creator_details?: Pick<
|
||||
IOrbisProfile['details'],
|
||||
'did' | 'metadata' | 'profile'
|
||||
>
|
||||
reply_to_details?: IOrbisMessageContent
|
||||
stream_id: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
declare interface IOrbisConversation {
|
||||
content: {
|
||||
recipients: string[]
|
||||
}
|
||||
@ -163,18 +547,14 @@ declare interface OrbisConversationInterface {
|
||||
last_message_timestamp: number
|
||||
last_timestamp_read: number
|
||||
recipients: string[]
|
||||
recipients_details: OrbisPostCreatorDetailsInterface[]
|
||||
recipients_details: IOrbisProfile['details'][]
|
||||
stream_id: string
|
||||
}
|
||||
|
||||
declare interface OrbisNotificationInterface {
|
||||
declare interface IOrbisNotification {
|
||||
content: {
|
||||
conversation_id: string
|
||||
encryptedMessage: {
|
||||
accessControlConditions: string
|
||||
encryptedString: string
|
||||
encryptedSymmetricKey: string
|
||||
}
|
||||
encryptedMessage: IOrbisEncryptedBody
|
||||
}
|
||||
family: string
|
||||
post_details: object
|
||||
@ -182,6 +562,6 @@ declare interface OrbisNotificationInterface {
|
||||
type: string
|
||||
user_notifying_details: {
|
||||
did: string
|
||||
profile: OrbisCreatorProfileInterface
|
||||
profile: IOrbisProfile['details']['profile']
|
||||
}
|
||||
}
|
||||
|
@ -12,11 +12,19 @@ export function didToAddress(did: string) {
|
||||
return _did[4]
|
||||
}
|
||||
|
||||
export function formatMessage(content: OrbisPostContentInterface): ReactNode {
|
||||
export function formatMessage(
|
||||
content: IOrbisPostContent,
|
||||
hideOverflow = false
|
||||
): ReactNode {
|
||||
if (!content || !content.body) return null
|
||||
|
||||
let { body }: { body: any } = content
|
||||
|
||||
if (hideOverflow && body.length > 285) {
|
||||
body = body.substring(0, 280)
|
||||
return body + '...'
|
||||
}
|
||||
|
||||
/** Replace all <br> generated by the postbox to \n to handle line breaks */
|
||||
body = reactStringReplace(body, '<br>', function (match, i) {
|
||||
return <br key={match + i} />
|
||||
|
24
src/@utils/throttle.ts
Normal file
24
src/@utils/throttle.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export function throttle<Args extends unknown[]>(
|
||||
fn: (...args: Args) => void,
|
||||
cooldown: number
|
||||
) {
|
||||
let lastArgs: Args | undefined
|
||||
|
||||
const run = () => {
|
||||
if (lastArgs) {
|
||||
fn(...lastArgs)
|
||||
lastArgs = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const throttled = (...args: Args) => {
|
||||
const isOnCooldown = !!lastArgs
|
||||
lastArgs = args
|
||||
if (isOnCooldown) {
|
||||
return
|
||||
}
|
||||
window.setTimeout(run, cooldown)
|
||||
}
|
||||
|
||||
return throttled
|
||||
}
|
27
src/components/@shared/Orbis/Comment/MasterPost.module.css
Normal file
27
src/components/@shared/Orbis/Comment/MasterPost.module.css
Normal file
@ -0,0 +1,27 @@
|
||||
.masterPost {
|
||||
padding: calc(var(--spacer) / 2) var(--spacer);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.showReplies {
|
||||
cursor: pointer;
|
||||
color: var(--brand-pink);
|
||||
font-size: var(--font-size-small);
|
||||
display: inline-flex;
|
||||
justify-content: flex-start;
|
||||
gap: calc(var(--spacer) / 4);
|
||||
margin-left: calc(
|
||||
calc(var(--font-size-large) * 1.5) + calc(var(--spacer) / 2)
|
||||
);
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.showReplies svg {
|
||||
fill: currentColor;
|
||||
width: var(--font-size-mini);
|
||||
margin-right: calc(var(--spacer) / 8);
|
||||
}
|
||||
|
||||
.showReplies.opened svg {
|
||||
transform: scaleY(-1);
|
||||
}
|
107
src/components/@shared/Orbis/Comment/MasterPost.tsx
Normal file
107
src/components/@shared/Orbis/Comment/MasterPost.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import styles from './MasterPost.module.css'
|
||||
import Post from './Post'
|
||||
import Replies from './Replies'
|
||||
import Caret from '@images/caret.svg'
|
||||
|
||||
export default function MasterPost({ post }: { post: IOrbisPost }) {
|
||||
const masterPost = useRef<HTMLDivElement | null>(null)
|
||||
const innerPostbox = useRef<HTMLDivElement | null>(null)
|
||||
const [showReplies, setShowReplies] = useState(false)
|
||||
const [replyTo, setReplyTo] = useState<IOrbisPost | null>(null)
|
||||
const [scrollToEl, setScrollToEl] = useState<HTMLElement | string | null>(
|
||||
null
|
||||
)
|
||||
|
||||
const deletePost = async (post: IOrbisPost) => {
|
||||
console.log('delete post:', post)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollToEl !== null) {
|
||||
const _scrollable: HTMLElement = masterPost.current.closest(
|
||||
'.comment-scrollable'
|
||||
)
|
||||
if (scrollToEl === 'masterPost') {
|
||||
setTimeout(() => {
|
||||
const _master = masterPost.current
|
||||
_scrollable.scrollTo({
|
||||
top: _master.offsetTop,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}, 500)
|
||||
} else if (scrollToEl === 'postbox') {
|
||||
setTimeout(() => {
|
||||
const _postbox = innerPostbox.current
|
||||
const halfScrollable = _scrollable.offsetHeight / 2
|
||||
const targetOffset = _postbox.offsetTop - halfScrollable
|
||||
_scrollable.scrollTo({
|
||||
top: targetOffset,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}, 500)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
const _el = scrollToEl as HTMLElement
|
||||
const halfScrollable = _scrollable.offsetHeight / 2
|
||||
const targetOffset = _el.offsetTop - halfScrollable
|
||||
_scrollable.scrollTo({
|
||||
top: targetOffset,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
setTimeout(() => {
|
||||
setScrollToEl(null)
|
||||
}, 500)
|
||||
}
|
||||
}, [scrollToEl])
|
||||
|
||||
useEffect(() => {
|
||||
if (!showReplies) setReplyTo(null)
|
||||
}, [showReplies])
|
||||
|
||||
return (
|
||||
<div ref={masterPost} className={styles.masterPost}>
|
||||
<Post
|
||||
post={post}
|
||||
onClickReply={() => {
|
||||
setReplyTo(post)
|
||||
setShowReplies(true)
|
||||
setScrollToEl('postbox')
|
||||
}}
|
||||
/>
|
||||
{post?.count_replies !== undefined && post?.count_replies > 0 && (
|
||||
<div
|
||||
className={`${styles.showReplies} ${
|
||||
showReplies ? styles.opened : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
setScrollToEl(!showReplies ? 'masterPost' : null)
|
||||
setShowReplies((prev) => !prev)
|
||||
}}
|
||||
>
|
||||
<Caret role="img" aria-label="Caret" />
|
||||
{showReplies ? 'Hide' : 'View'} replies
|
||||
</div>
|
||||
)}
|
||||
{showReplies && (
|
||||
<div>
|
||||
<Replies
|
||||
master={post}
|
||||
innerPostbox={innerPostbox}
|
||||
replyTo={replyTo}
|
||||
setReplyTo={(post) => {
|
||||
setReplyTo(post)
|
||||
if (post) setScrollToEl('postbox')
|
||||
}}
|
||||
onNewPost={(el: HTMLElement | null) => setScrollToEl(el)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,13 +1,26 @@
|
||||
@keyframes pulse {
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.post {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: calc(var(--spacer) / 2) var(--spacer);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.post.deleted {
|
||||
opacity: 40%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
.post:last-of-type {
|
||||
border-bottom: 0;
|
||||
}
|
||||
@ -42,7 +55,36 @@
|
||||
margin-right: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.reactions {
|
||||
.body {
|
||||
position: relative;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.readMore {
|
||||
cursor: pointer;
|
||||
color: var(--brand-pink);
|
||||
font-size: var(--font-size-small);
|
||||
display: inline-flex;
|
||||
justify-content: flex-start;
|
||||
gap: calc(var(--spacer) / 4);
|
||||
margin: calc(var(--spacer) / 2) auto 0;
|
||||
}
|
||||
|
||||
.readMore svg {
|
||||
fill: currentColor;
|
||||
width: var(--font-size-mini);
|
||||
margin-right: calc(var(--spacer) / 8);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.reactions,
|
||||
.menu {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
@ -50,7 +92,9 @@
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.reactions button {
|
||||
.reactions button,
|
||||
.menu button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
@ -61,10 +105,42 @@
|
||||
}
|
||||
|
||||
.reactions button:hover,
|
||||
.reactions button.reacted {
|
||||
.reactions button.reacted,
|
||||
.menu button:hover {
|
||||
color: var(--brand-pink);
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.options button {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
padding: calc(var(--spacer) / 6) calc(var(--spacer) / 3);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.postEdit {
|
||||
color: var(--font-color-text);
|
||||
}
|
||||
|
||||
.postEdit:hover {
|
||||
background: var(--background-highlight);
|
||||
}
|
||||
|
||||
.postDelete {
|
||||
color: var(--brand-alert-red);
|
||||
}
|
||||
|
||||
.postDelete:hover {
|
||||
color: var(--font-color-text);
|
||||
background: var(--brand-alert-red);
|
||||
}
|
||||
|
||||
.reactions svg {
|
||||
fill: currentColor;
|
||||
width: var(--font-size-large);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, ReactNode } from 'react'
|
||||
import React, { useState, useMemo, useEffect, ReactNode } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { accountTruncate } from '@utils/web3'
|
||||
import { didToAddress, formatMessage } from '@utils/orbis'
|
||||
@ -6,138 +6,233 @@ import Avatar from '@shared/atoms/Avatar'
|
||||
import Time from '@shared/atoms/Time'
|
||||
import { useOrbis } from '@context/Orbis'
|
||||
import styles from './Post.module.css'
|
||||
import Tooltip from '@shared/atoms/Tooltip'
|
||||
import Postbox from './Postbox'
|
||||
|
||||
import Reply from '@images/reply.svg'
|
||||
import ThumbsUp from '@images/thumbsup.svg'
|
||||
import ThumbsDown from '@images/thumbsdown.svg'
|
||||
import Laugh from '@images/laugh.svg'
|
||||
import Ellipsis from '@images/ellipsis.svg'
|
||||
import Caret from '@images/caret.svg'
|
||||
|
||||
type Reactions = Pick<
|
||||
IOrbisPost,
|
||||
'count_likes' | 'count_downvotes' | 'count_haha'
|
||||
>
|
||||
|
||||
export default function Post({
|
||||
post,
|
||||
showProfile = true
|
||||
onClickReply
|
||||
}: {
|
||||
post: OrbisPostInterface
|
||||
showProfile?: boolean
|
||||
post: IOrbisPost
|
||||
onClickReply: (value: IOrbisPost | boolean) => void
|
||||
}) {
|
||||
const { orbis, account } = useOrbis()
|
||||
|
||||
const [address, setAddress] = useState('')
|
||||
const [name, setName] = useState('')
|
||||
const [postClone, setPostClone] = useState<IOrbisPost>({ ...post })
|
||||
const [parsedBody, setParsedBody] = useState<ReactNode>()
|
||||
const [reacted, setReacted] = useState('')
|
||||
const [reacted, setReacted] = useState<string>('')
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [isDeleted, setIsDeleted] = useState<number>(0)
|
||||
const [hideOverflow, setHideOverflow] = useState<boolean>(
|
||||
postClone.content.body.length >= 285
|
||||
)
|
||||
|
||||
const reactions = [
|
||||
// {
|
||||
// ctx: 'reply',
|
||||
// countKey: 'count_replies',
|
||||
// count: post.count_replies,
|
||||
// title: 'Reply',
|
||||
// icon:
|
||||
// },
|
||||
{
|
||||
ctx: 'like',
|
||||
countKey: 'count_likes',
|
||||
count: post.count_likes,
|
||||
title: 'Like',
|
||||
icon: <ThumbsUp role="img" aria-label="Like" />
|
||||
},
|
||||
{
|
||||
ctx: 'downvote',
|
||||
countKey: 'count_downvotes',
|
||||
count: post.count_downvotes,
|
||||
title: 'Downvote',
|
||||
icon: <ThumbsDown role="img" aria-label="Downvote" />
|
||||
},
|
||||
{
|
||||
ctx: 'haha',
|
||||
countKey: 'count_haha',
|
||||
count: post.count_haha,
|
||||
title: 'HA HA',
|
||||
icon: <Laugh role="img" aria-label="Downvote" />
|
||||
}
|
||||
]
|
||||
|
||||
const getUserReaction = async () => {
|
||||
if (!account?.did || !post?.stream_id) return
|
||||
|
||||
const { data, error } = await orbis.api
|
||||
.from('orbis_reactions')
|
||||
.select('type')
|
||||
.eq('post_id', post.stream_id)
|
||||
.eq('creator', account.did)
|
||||
|
||||
if (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
if (data) {
|
||||
if (data.length > 0) {
|
||||
console.log(data[0].type, post.content.body)
|
||||
setReacted(data[0].type)
|
||||
const reactions = useMemo(() => {
|
||||
const items = [
|
||||
{
|
||||
type: 'like',
|
||||
count: postClone.count_likes,
|
||||
title: 'Like',
|
||||
icon: <ThumbsUp role="img" aria-label="Like" />
|
||||
},
|
||||
{
|
||||
type: 'downvote',
|
||||
count: postClone.count_downvotes,
|
||||
title: 'Downvote',
|
||||
icon: <ThumbsDown role="img" aria-label="Downvote" />
|
||||
},
|
||||
{
|
||||
type: 'haha',
|
||||
count: postClone.count_haha,
|
||||
title: 'HA HA!',
|
||||
icon: <Laugh role="img" aria-label="Downvote" />
|
||||
}
|
||||
]
|
||||
|
||||
return items
|
||||
}, [postClone])
|
||||
|
||||
const handleReaction = async (type: string) => {
|
||||
// Quick return if already reacted
|
||||
if (type === reacted) return
|
||||
|
||||
// Optimistically increase reaction count
|
||||
setReacted(type)
|
||||
const _post: IOrbisPost = { ...postClone }
|
||||
const keys = {
|
||||
like: 'count_likes' as keyof Reactions,
|
||||
haha: 'count_haha' as keyof Reactions,
|
||||
downvote: 'count_downvotes' as keyof Reactions
|
||||
}
|
||||
// Decrease old reaction
|
||||
if (reacted) {
|
||||
_post[keys[reacted as keyof typeof keys]] -= 1
|
||||
}
|
||||
// Increase new reaction
|
||||
_post[keys[type as keyof typeof keys]] += 1
|
||||
setPostClone({ ..._post })
|
||||
const res = await orbis.react(postClone.stream_id, type)
|
||||
// Revert back if failed
|
||||
if (res.status !== 200) {
|
||||
setPostClone({ ...postClone })
|
||||
}
|
||||
}
|
||||
|
||||
const handleReaction = (type: string) => {
|
||||
console.log(type)
|
||||
const callbackEdit = async (content: IOrbisPostContent) => {
|
||||
setPostClone({
|
||||
...postClone,
|
||||
content,
|
||||
count_commits: postClone.count_commits + 1
|
||||
})
|
||||
setIsEditing(false)
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (
|
||||
confirm(
|
||||
'Are you sure you want to delete this post?\r\nIf you ask for deletion your post might be removed from the Ceramic nodes hosting it.'
|
||||
)
|
||||
) {
|
||||
setIsDeleted(1)
|
||||
await orbis.deletePost(postClone.stream_id)
|
||||
setIsDeleted(2)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!post) return
|
||||
|
||||
const address =
|
||||
post?.creator_details?.metadata?.address ||
|
||||
didToAddress(post?.creator_details?.did)
|
||||
|
||||
setAddress(address)
|
||||
|
||||
if (post?.creator_details?.metadata?.ensName) {
|
||||
setName(post?.creator_details?.metadata?.ensName)
|
||||
} else if (post?.creator_details?.profile?.username) {
|
||||
setName(post?.creator_details?.profile?.username)
|
||||
} else {
|
||||
setName(accountTruncate(address))
|
||||
const getUserReaction = async () => {
|
||||
if (!account) return
|
||||
const { data } = await orbis.getReaction(postClone.stream_id, account.did)
|
||||
if (data) setReacted(data.type)
|
||||
}
|
||||
|
||||
setParsedBody(formatMessage(post.content))
|
||||
if (postClone) {
|
||||
const address =
|
||||
postClone?.creator_details?.metadata?.address ||
|
||||
didToAddress(postClone?.creator_details?.did)
|
||||
|
||||
getUserReaction()
|
||||
}, [post])
|
||||
setAddress(address)
|
||||
setParsedBody(formatMessage(postClone.content, hideOverflow))
|
||||
|
||||
useEffect(() => {
|
||||
if (orbis && account) getUserReaction()
|
||||
}, [orbis, account])
|
||||
if (account) getUserReaction()
|
||||
}
|
||||
}, [postClone, account, orbis, hideOverflow])
|
||||
|
||||
return (
|
||||
<div className={styles.post}>
|
||||
<div
|
||||
className={`${styles.post} ${postClone.stream_id} ${
|
||||
postClone.stream_id.startsWith('new_post-') || isDeleted === 1
|
||||
? styles.pulse
|
||||
: ''
|
||||
} ${isDeleted === 2 ? styles.deleted : ''}`}
|
||||
>
|
||||
<Avatar accountId={address} className={styles.blockies} />
|
||||
<div className={styles.content}>
|
||||
{showProfile && (
|
||||
<div className={styles.profile}>
|
||||
<Link href={`/profile/${address}`}>
|
||||
<a className={styles.name}>{name}</a>
|
||||
</Link>
|
||||
<span>•</span>
|
||||
<div className={styles.metadata}>{accountTruncate(address)}</div>
|
||||
<span>•</span>
|
||||
<div className={styles.metadata}>
|
||||
<Time date={post.timestamp.toString()} isUnix={true} relative />
|
||||
</div>
|
||||
<div className={styles.profile}>
|
||||
<Link href={`/profile/${address}`}>
|
||||
<a className={styles.name}>{accountTruncate(address)}</a>
|
||||
</Link>
|
||||
<span>•</span>
|
||||
<div className={styles.metadata}>
|
||||
{postClone?.creator_details?.metadata?.ensName ||
|
||||
accountTruncate(address)}
|
||||
</div>
|
||||
<span>•</span>
|
||||
<div className={styles.metadata}>
|
||||
<Time
|
||||
date={post.timestamp.toString()}
|
||||
isUnix={true}
|
||||
displayFormat="Pp"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isDeleted === 2 ? (
|
||||
<div className={styles.body}>-- This post is deleted -- </div>
|
||||
) : !isEditing ? (
|
||||
<div className={styles.body}>{parsedBody}</div>
|
||||
) : (
|
||||
<Postbox
|
||||
context={postClone?.context}
|
||||
editPost={postClone}
|
||||
callback={callbackEdit}
|
||||
/>
|
||||
)}
|
||||
{hideOverflow && (
|
||||
<div
|
||||
className={styles.readMore}
|
||||
onClick={() => {
|
||||
setHideOverflow(false)
|
||||
}}
|
||||
>
|
||||
<Caret role="img" aria-label="Caret" />
|
||||
Read more
|
||||
</div>
|
||||
)}
|
||||
{isDeleted !== 2 && (
|
||||
<div className={styles.footer}>
|
||||
<div className={styles.reactions}>
|
||||
<button title="Reply" onClick={() => onClickReply(post)}>
|
||||
<Reply role="img" aria-label="Reply" />
|
||||
<span>Reply</span>
|
||||
</button>
|
||||
{reactions.map(({ type, count, icon, title }) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => handleReaction(type)}
|
||||
title={title}
|
||||
className={type === reacted ? styles.reacted : ''}
|
||||
>
|
||||
{icon}
|
||||
<span>{count}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{postClone.creator_details.did === account?.did && (
|
||||
<div className={styles.menu}>
|
||||
<Tooltip
|
||||
content={
|
||||
<div className={styles.options}>
|
||||
<button
|
||||
className={styles.postEdit}
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
Edit post
|
||||
</button>
|
||||
<button
|
||||
className={styles.postDelete}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete post
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
zIndex={21}
|
||||
placement={'top'}
|
||||
>
|
||||
<button
|
||||
title="Options"
|
||||
onClick={() => console.log('clicked')}
|
||||
>
|
||||
<Ellipsis role="img" aria-label="Options" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.message}>{parsedBody}</div>
|
||||
<div className={styles.reactions}>
|
||||
{reactions.map(({ ctx, count, icon, title }) => (
|
||||
<button
|
||||
key={ctx}
|
||||
onClick={() => handleReaction(ctx)}
|
||||
title={title}
|
||||
className={ctx === reacted && styles.reacted}
|
||||
>
|
||||
{icon}
|
||||
<span>{count}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,13 +1,27 @@
|
||||
.postbox {
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.postbox .postboxInput {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: calc(var(--spacer) / 4);
|
||||
margin-bottom: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.postbox .editable {
|
||||
padding: calc(var(--spacer) / 4);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
min-height: 80px;
|
||||
margin-bottom: calc(var(--spacer) / 4);
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
flex-grow: 1;
|
||||
outline: none;
|
||||
max-height: 80px;
|
||||
overflow-y: auto;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.postbox .editable:empty:before {
|
||||
@ -16,10 +30,40 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.postbox .editable::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.postbox .sendButtonWrap {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.postbox .postboxInput {
|
||||
.replyto {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: calc(var(--spacer) / 2);
|
||||
padding: calc(var(--spacer) / 2);
|
||||
font-size: var(--font-size-small);
|
||||
background: var(--background-highlight);
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-top-right-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.replytoDetails {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.replytoCancel {
|
||||
flex-shrink: 0;
|
||||
font-size: var(--font-size-large);
|
||||
line-height: 1;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: currentColor;
|
||||
outline: none;
|
||||
}
|
||||
|
@ -1,29 +1,41 @@
|
||||
import React, { useRef, useState } from 'react'
|
||||
import Button from '@shared/atoms/Button'
|
||||
import React, { useRef, useState, KeyboardEvent } from 'react'
|
||||
import styles from './Postbox.module.css'
|
||||
import { useOrbis } from '@context/Orbis'
|
||||
import EmojiPicker from '../EmojiPicker'
|
||||
import { EmojiClickData } from 'emoji-picker-react'
|
||||
import { useOrbis } from '@context/Orbis'
|
||||
import Button from '@shared/atoms/Button'
|
||||
import EmojiPicker from '../EmojiPicker'
|
||||
import { accountTruncate } from '@utils/web3'
|
||||
import { didToAddress } from '@utils/orbis'
|
||||
|
||||
export default function Postbox({
|
||||
assetId,
|
||||
context,
|
||||
placeholder = 'Share your post here...',
|
||||
callbackPost
|
||||
replyTo = null,
|
||||
editPost = null,
|
||||
enterToSend = false,
|
||||
cancelReplyTo,
|
||||
callback
|
||||
}: {
|
||||
placeholder: string
|
||||
assetId: string
|
||||
callbackPost: (post: OrbisPostInterface) => void
|
||||
context: string
|
||||
placeholder?: string
|
||||
replyTo?: IOrbisPost
|
||||
editPost?: IOrbisPost
|
||||
enterToSend?: boolean
|
||||
cancelReplyTo?: () => void
|
||||
callback: (value: any) => void
|
||||
}) {
|
||||
const [post, setPost] = useState('')
|
||||
const [focusOffset, setFocusOffset] = useState(null)
|
||||
const [focusNode, setFocusNode] = useState(null)
|
||||
const [focusOffset, setFocusOffset] = useState<number | undefined>()
|
||||
const [focusNode, setFocusNode] = useState<Node | undefined>()
|
||||
|
||||
const postBoxArea = useRef(null)
|
||||
const { orbis, account } = useOrbis()
|
||||
|
||||
const saveCaretPos = (_sel: { focusOffset: number; focusNode: Node }) => {
|
||||
setFocusOffset(_sel.focusOffset)
|
||||
setFocusNode(_sel.focusNode)
|
||||
const saveCaretPos = () => {
|
||||
const sel = document.getSelection()
|
||||
if (sel) {
|
||||
setFocusOffset(sel.focusOffset)
|
||||
setFocusNode(sel.focusNode)
|
||||
}
|
||||
}
|
||||
|
||||
const restoreCaretPos = () => {
|
||||
@ -32,88 +44,127 @@ export default function Postbox({
|
||||
sel.collapse(focusNode, focusOffset)
|
||||
}
|
||||
|
||||
const handleInput = (e: any) => {
|
||||
setPost(e.currentTarget.innerText)
|
||||
saveCaretPos(document.getSelection())
|
||||
const share = async () => {
|
||||
if (!account) return false
|
||||
|
||||
const body = postBoxArea.current.innerText
|
||||
|
||||
// Cleaning up mentions
|
||||
// const _mentions = mentions.filter((o) => body.includes(o.username))
|
||||
|
||||
if (editPost) {
|
||||
const newContent = { ...editPost.content, body }
|
||||
if (callback) callback(newContent)
|
||||
await orbis.editPost(editPost.stream_id, newContent)
|
||||
} else {
|
||||
const content = {
|
||||
body,
|
||||
context,
|
||||
master: replyTo ? replyTo.master || replyTo.stream_id : undefined,
|
||||
reply_to: replyTo ? replyTo.stream_id : undefined
|
||||
// mentions: _mentions || undefined
|
||||
}
|
||||
|
||||
const timestamp = Math.floor(Date.now() / 1000)
|
||||
|
||||
const _callbackContent = {
|
||||
content,
|
||||
context,
|
||||
creator: account.did,
|
||||
creator_details: {
|
||||
did: account.did,
|
||||
profile: account.details?.profile,
|
||||
metadata: account.details?.metadata
|
||||
},
|
||||
stream_id: 'new_post-' + timestamp,
|
||||
timestamp,
|
||||
master: replyTo ? replyTo.master || replyTo.stream_id : null,
|
||||
reply_to: replyTo ? replyTo.stream_id : null,
|
||||
reply_to_creator_details: replyTo ? replyTo.creator_details : null,
|
||||
reply_to_details: replyTo ? replyTo.content : null,
|
||||
count_commits: 1,
|
||||
count_likes: 0,
|
||||
count_haha: 0,
|
||||
count_downvotes: 0,
|
||||
count_replies: 0,
|
||||
type: replyTo ? 'reply' : null
|
||||
}
|
||||
|
||||
console.log(_callbackContent)
|
||||
|
||||
if (callback) callback(_callbackContent)
|
||||
postBoxArea.current.innerText = ''
|
||||
|
||||
const res = await orbis.createPost(content)
|
||||
|
||||
if (res.status === 200) {
|
||||
_callbackContent.stream_id = res.doc
|
||||
if (callback) callback(_callbackContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!e.key) return
|
||||
|
||||
if (enterToSend && e.key === 'Enter' && !e.shiftKey) {
|
||||
// Don't generate a new line
|
||||
e.preventDefault()
|
||||
share()
|
||||
}
|
||||
}
|
||||
|
||||
const onEmojiClick = (emojiData: EmojiClickData) => {
|
||||
restoreCaretPos()
|
||||
document.execCommand('insertHTML', false, emojiData.emoji)
|
||||
// setPost((prevInput) => prevInput + emojiData.emoji)
|
||||
// postBoxArea.current.innerText += emojiData.emoji
|
||||
}
|
||||
|
||||
const createPost = async () => {
|
||||
// console.log(post)
|
||||
const _callbackContent: OrbisPostInterface = {
|
||||
creator: account.did,
|
||||
creator_details: {
|
||||
did: account.did,
|
||||
profile: account.profile
|
||||
},
|
||||
stream_id: null,
|
||||
content: {
|
||||
body: post
|
||||
},
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
count_replies: 0,
|
||||
count_likes: 0,
|
||||
count_downvotes: 0,
|
||||
count_haha: 0
|
||||
}
|
||||
console.log(_callbackContent)
|
||||
callbackPost(_callbackContent)
|
||||
|
||||
const res = await orbis.createPost({ body: post, context: assetId })
|
||||
|
||||
if (res.status === 200) {
|
||||
console.log('success with,', res)
|
||||
setPost(null)
|
||||
if (postBoxArea.current) {
|
||||
postBoxArea.current.textContent = ''
|
||||
postBoxArea.current.focus()
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
const { data, error } = await orbis.getPost(res.doc)
|
||||
console.log(data)
|
||||
if (data) {
|
||||
callbackPost(data)
|
||||
}
|
||||
if (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}, 2000)
|
||||
if (focusOffset === undefined || focusNode === undefined) {
|
||||
postBoxArea.current.innerText += emojiData.emoji
|
||||
} else {
|
||||
restoreCaretPos()
|
||||
document.execCommand('insertHTML', false, emojiData.emoji)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.postbox}>
|
||||
<div className={styles.postboxInput}>
|
||||
<div
|
||||
id="postbox-area"
|
||||
ref={postBoxArea}
|
||||
className={styles.editable}
|
||||
contentEditable={true}
|
||||
data-placeholder={placeholder}
|
||||
onInput={handleInput}
|
||||
/>
|
||||
<EmojiPicker onEmojiClick={onEmojiClick} />
|
||||
</div>
|
||||
<div className={styles.sendButtonWrap}>
|
||||
<Button
|
||||
style="primary"
|
||||
type="submit"
|
||||
size="small"
|
||||
disabled={false}
|
||||
onClick={createPost}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
<div className={styles.postbox}>
|
||||
{replyTo && (
|
||||
<div className={styles.replyto}>
|
||||
<div className={styles.replytoDetails}>
|
||||
Replying to{' '}
|
||||
<strong>
|
||||
{replyTo?.creator_details?.metadata?.ensName ||
|
||||
accountTruncate(didToAddress(replyTo?.creator_details?.did))}
|
||||
</strong>
|
||||
<br />
|
||||
{replyTo.content?.body}
|
||||
</div>
|
||||
<button className={styles.replytoCancel} onClick={cancelReplyTo}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.postboxInput}>
|
||||
<div
|
||||
id="postbox-area"
|
||||
ref={postBoxArea}
|
||||
className={styles.editable}
|
||||
contentEditable={true}
|
||||
data-placeholder={placeholder}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={saveCaretPos}
|
||||
onMouseUp={saveCaretPos}
|
||||
/>
|
||||
<EmojiPicker onEmojiClick={onEmojiClick} />
|
||||
</div>
|
||||
</>
|
||||
<div className={styles.sendButtonWrap}>
|
||||
<Button
|
||||
style="primary"
|
||||
type="submit"
|
||||
size="small"
|
||||
disabled={false}
|
||||
onClick={share}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,26 +1,26 @@
|
||||
import React from 'react'
|
||||
import Loader from '@shared/atoms/Loader'
|
||||
import Button from '@shared/atoms/Button'
|
||||
import Post from './Post'
|
||||
import MasterPost from './MasterPost'
|
||||
import styles from './Posts.module.css'
|
||||
|
||||
export default function Posts({
|
||||
context,
|
||||
posts,
|
||||
loadPosts,
|
||||
fetchPosts,
|
||||
hasMore,
|
||||
loading
|
||||
}: {
|
||||
posts: OrbisPostInterface[]
|
||||
loadPosts: () => void
|
||||
context: string
|
||||
posts: IOrbisPost[]
|
||||
fetchPosts: () => Promise<void>
|
||||
hasMore: boolean
|
||||
loading: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.posts}>
|
||||
<div>
|
||||
{posts.length > 0 &&
|
||||
posts.map((post, index) => <Post key={index} post={post} />)}
|
||||
</div>
|
||||
{posts.length > 0 &&
|
||||
posts.map((post, index) => <MasterPost key={index} post={post} />)}
|
||||
|
||||
{loading ? (
|
||||
<div className={styles.loader}>
|
||||
@ -34,7 +34,7 @@ export default function Posts({
|
||||
|
||||
{!loading && hasMore && (
|
||||
<div className={styles.loadMore}>
|
||||
<Button style="text" size="small" onClick={loadPosts}>
|
||||
<Button style="text" size="small" onClick={fetchPosts}>
|
||||
Load More
|
||||
</Button>
|
||||
</div>
|
||||
|
19
src/components/@shared/Orbis/Comment/Replies.module.css
Normal file
19
src/components/@shared/Orbis/Comment/Replies.module.css
Normal file
@ -0,0 +1,19 @@
|
||||
.replies {
|
||||
border-left: 1px solid var(--border-color);
|
||||
margin-left: calc(var(--spacer) / 2);
|
||||
padding-left: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.replies:first-child {
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.repliesItem {
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
margin-bottom: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.repliesPostbox {
|
||||
margin-left: calc(var(--spacer) / 2);
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
}
|
136
src/components/@shared/Orbis/Comment/Replies.tsx
Normal file
136
src/components/@shared/Orbis/Comment/Replies.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import React, { useState, useEffect, useMemo, useRef, LegacyRef } from 'react'
|
||||
import styles from './Replies.module.css'
|
||||
import { useOrbis } from '@context/Orbis'
|
||||
import Post from './Post'
|
||||
import Postbox from './Postbox'
|
||||
|
||||
const RepliesGroup = ({
|
||||
items,
|
||||
replies,
|
||||
setReplyTo
|
||||
}: {
|
||||
items: IOrbisPost[]
|
||||
replies: Record<string, IOrbisPost[]>
|
||||
setReplyTo: (value: IOrbisPost | boolean) => void
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{items.map((item) => (
|
||||
<div key={item.stream_id} className={styles.replies}>
|
||||
<div className={styles.repliesItem}>
|
||||
<Post post={item} onClickReply={() => setReplyTo(item)} />
|
||||
</div>
|
||||
{replies[item.stream_id] !== undefined && (
|
||||
<RepliesGroup
|
||||
items={replies[item.stream_id]}
|
||||
replies={replies}
|
||||
setReplyTo={setReplyTo}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Replies = ({
|
||||
master,
|
||||
replyTo,
|
||||
innerPostbox,
|
||||
setReplyTo,
|
||||
onNewPost
|
||||
}: {
|
||||
master: IOrbisPost
|
||||
replyTo: IOrbisPost
|
||||
innerPostbox: LegacyRef<HTMLDivElement> | null
|
||||
setReplyTo: (value: IOrbisPost) => void
|
||||
onNewPost: (el: HTMLElement | null) => void
|
||||
}) => {
|
||||
const { orbis } = useOrbis()
|
||||
const mainGroup = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const [posts, setPosts] = useState<IOrbisPost[]>([])
|
||||
const [currentPage, setCurrentPage] = useState<number>(0)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
// const [hasMore, setHasMore] = useState<boolean>(false)
|
||||
|
||||
const repliesGroups = useMemo(() => {
|
||||
const grouped = posts.reduce((result, a) => {
|
||||
result[a.reply_to] = [...(result[a.reply_to] || []), a]
|
||||
return result
|
||||
}, {} as Record<string, IOrbisPost[]>)
|
||||
return grouped
|
||||
}, [posts])
|
||||
|
||||
const getPosts = async () => {
|
||||
if (!master || isLoading) return
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
const { data, error } = await orbis.getPosts(
|
||||
{
|
||||
context: master.context,
|
||||
master: master.stream_id
|
||||
},
|
||||
currentPage
|
||||
)
|
||||
|
||||
if (data) {
|
||||
data.reverse()
|
||||
setPosts([...posts, ...data])
|
||||
setCurrentPage((prev) => prev + 1)
|
||||
// setHasMore(data.length >= 50)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const callback = (newPost: IOrbisPost) => {
|
||||
const _posts = [...posts, newPost]
|
||||
setPosts(_posts)
|
||||
setReplyTo(null)
|
||||
|
||||
// Try scroll to newly created post
|
||||
if (newPost.stream_id.startsWith('new_post-')) {
|
||||
setTimeout(() => {
|
||||
const el: HTMLElement = mainGroup.current?.querySelector(
|
||||
`.${newPost.stream_id}`
|
||||
)
|
||||
onNewPost(el)
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (master) getPosts()
|
||||
}, [master])
|
||||
|
||||
return (
|
||||
<div ref={mainGroup}>
|
||||
{repliesGroups[master.stream_id] !== undefined && (
|
||||
<RepliesGroup
|
||||
items={repliesGroups[master.stream_id]}
|
||||
replies={repliesGroups}
|
||||
setReplyTo={setReplyTo}
|
||||
/>
|
||||
)}
|
||||
{replyTo && (
|
||||
<div ref={innerPostbox} className={styles.repliesPostbox}>
|
||||
<Postbox
|
||||
context={master.context}
|
||||
replyTo={replyTo}
|
||||
placeholder="Reply this comment..."
|
||||
cancelReplyTo={() => setReplyTo(null)}
|
||||
callback={callback}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Replies
|
@ -5,7 +5,7 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 620px;
|
||||
max-height: 720px;
|
||||
}
|
||||
|
||||
.header {
|
||||
@ -33,5 +33,6 @@
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
@ -5,20 +5,24 @@ import Postbox from './Postbox'
|
||||
import CommentIcon from '@images/comment.svg'
|
||||
import { useOrbis } from '@context/Orbis'
|
||||
|
||||
export default function Comment({ asset }: { asset: AssetExtended }) {
|
||||
export default function Comment({ context }: { context: string }) {
|
||||
const { orbis } = useOrbis()
|
||||
const [posts, setPosts] = useState<OrbisPostInterface[]>([])
|
||||
const [posts, setPosts] = useState<IOrbisPost[]>([])
|
||||
const [page, setPage] = useState(0)
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const loadPosts = async () => {
|
||||
const _context =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? 'kjzl6cwe1jw145gun3sei0a4puw586yxa614le1tfh434y7quv2wsm0ivhbge7x'
|
||||
: context
|
||||
|
||||
const fetchPosts = async () => {
|
||||
setLoading(true)
|
||||
// const context =
|
||||
// process.env.NODE_ENV === 'development'
|
||||
// ? 'kjzl6cwe1jw149vvm1f8p9qlohhtkjuc302f22mipq95q7mevdljgx3tv9swujy'
|
||||
// : asset?.id
|
||||
const { data, error } = await orbis.getPosts({ context: asset?.id }, page)
|
||||
const { data, error } = await orbis.getPosts(
|
||||
{ context: _context, algorithm: 'all-context-master-posts' },
|
||||
page
|
||||
)
|
||||
if (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@ -35,12 +39,12 @@ export default function Comment({ asset }: { asset: AssetExtended }) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (asset?.id && !posts.length && orbis) {
|
||||
loadPosts()
|
||||
if (context && !posts.length && orbis) {
|
||||
fetchPosts()
|
||||
}
|
||||
}, [asset, posts, orbis])
|
||||
}, [context, posts, orbis])
|
||||
|
||||
const callbackPost = (nPost: OrbisPostInterface) => {
|
||||
const callback = (nPost: IOrbisPost) => {
|
||||
// console.log(nPost)
|
||||
if (nPost.stream_id) {
|
||||
// Search and replace
|
||||
@ -59,10 +63,6 @@ export default function Comment({ asset }: { asset: AssetExtended }) {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log(posts)
|
||||
}, [posts])
|
||||
|
||||
return (
|
||||
<div className={styles.comment}>
|
||||
<div className={styles.header}>
|
||||
@ -71,16 +71,17 @@ export default function Comment({ asset }: { asset: AssetExtended }) {
|
||||
</div>
|
||||
<div className={styles.postBox}>
|
||||
<Postbox
|
||||
callbackPost={callbackPost}
|
||||
assetId={asset?.id}
|
||||
callback={callback}
|
||||
context={_context}
|
||||
placeholder="Share your comment here..."
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={`${styles.content} comment-scrollable`}>
|
||||
<Posts
|
||||
context={_context}
|
||||
posts={posts}
|
||||
loading={loading}
|
||||
loadPosts={loadPosts}
|
||||
fetchPosts={fetchPosts}
|
||||
hasMore={hasMore}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,61 +0,0 @@
|
||||
.chatToolbar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
background-color: var(--background-content);
|
||||
padding: calc(var(--spacer) / 2);
|
||||
border-top: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.chatMessage {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.chatMessage div {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.chatMessage label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
right: calc(var(--spacer) / 4);
|
||||
background-color: var(--background-content);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--font-color-text);
|
||||
font-size: var(--font-size-title);
|
||||
transition: color 200ms ease;
|
||||
}
|
||||
|
||||
.button:hover,
|
||||
.button:focus {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.buttonIcon {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.inputWrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inputWrap input {
|
||||
transition: none;
|
||||
padding-right: calc(var(--spacer) * 1.3);
|
||||
}
|
||||
|
||||
.emojiPicker {
|
||||
position: absolute;
|
||||
bottom: calc(var(--spacer) * 1.7);
|
||||
left: calc(var(--spacer) / 2);
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import styles from './ChatToolbar.module.css'
|
||||
import SendIcon from '@images/send.svg'
|
||||
import Input from '@shared/FormInput'
|
||||
import { useOrbis } from '@context/Orbis'
|
||||
import EmojiPicker from '../EmojiPicker'
|
||||
import { EmojiClickData } from 'emoji-picker-react'
|
||||
|
||||
export default function ChatToolbar({
|
||||
callbackMessage
|
||||
}: {
|
||||
callbackMessage: (post: OrbisPostInterface) => void
|
||||
}) {
|
||||
const { orbis, conversationId, account } = useOrbis()
|
||||
const [content, setContent] = useState<string>('')
|
||||
|
||||
const onEmojiClick = (emojiData: EmojiClickData) => {
|
||||
setContent((prevInput) => prevInput + emojiData.emoji)
|
||||
}
|
||||
|
||||
const sendMessage = async () => {
|
||||
const _callbackMessage: OrbisPostInterface = {
|
||||
creator: account.did,
|
||||
creator_details: {
|
||||
did: account.did,
|
||||
profile: account.profile
|
||||
},
|
||||
stream_id: null,
|
||||
content: {
|
||||
body: content
|
||||
},
|
||||
timestamp: Date.now()
|
||||
}
|
||||
callbackMessage(_callbackMessage)
|
||||
setContent('')
|
||||
|
||||
const res = await orbis.sendMessage({
|
||||
conversation_id: conversationId,
|
||||
body: content
|
||||
})
|
||||
|
||||
if (res.status === 200) {
|
||||
console.log('succes send message with,', res)
|
||||
setContent('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.chatToolbar}>
|
||||
<div className={styles.chatMessage}>
|
||||
<div className={styles.inputWrap}>
|
||||
<Input
|
||||
type="input"
|
||||
name="message"
|
||||
size="small"
|
||||
placeholder="Type Message"
|
||||
value={content}
|
||||
onChange={(e: any) => setContent(e.target.value)}
|
||||
/>
|
||||
<EmojiPicker onEmojiClick={onEmojiClick} />
|
||||
</div>
|
||||
<button className={styles.button} onClick={sendMessage}>
|
||||
<SendIcon className={styles.buttonIcon} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,9 +1,61 @@
|
||||
.conversationBox {
|
||||
padding: calc(var(--spacer) / 2);
|
||||
height: calc(100% - (var(--spacer) * 3.5));
|
||||
.conversation {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: calc(var(--spacer) / 4);
|
||||
text-align: center;
|
||||
color: var(--color-secondary);
|
||||
-webkit-animation: pulse 2s ease-in-out 0s infinite forwards;
|
||||
-moz-animation: pulse 2s ease-in-out 0s infinite forwards;
|
||||
animation: pulse 2s ease-in-out 0s infinite forwards;
|
||||
}
|
||||
|
||||
.connectLit {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: calc(var(--spacer) / 2);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.connectLit p {
|
||||
text-align: center;
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.messages {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.messages > .scrollContent {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: calc(var(--spacer) / 2) calc(var(--spacer) / 2)
|
||||
calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.newMessagesBadge {
|
||||
position: absolute;
|
||||
height: auto;
|
||||
bottom: calc(var(--spacer) / 2);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--brand-pink);
|
||||
color: var(--brand-white);
|
||||
padding: calc(var(--spacer) / 4) calc(var(--spacer) / 2);
|
||||
border: 0;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-text);
|
||||
box-shadow: 0 2px 4px var(--box-shadow-color);
|
||||
}
|
||||
|
||||
.message {
|
||||
@ -59,3 +111,15 @@
|
||||
.message:not(.showTime).left + .message.left .chatBubble {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,69 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { useOrbis } from '@context/Orbis'
|
||||
import { useIsMounted } from '@hooks/useIsMounted'
|
||||
import { useInterval } from '@hooks/useInterval'
|
||||
import { throttle } from '@utils/throttle'
|
||||
import Time from '@shared/atoms/Time'
|
||||
import Button from '@shared/atoms/Button'
|
||||
import DecryptedMessage from './DecryptedMessage'
|
||||
import Postbox from './Postbox'
|
||||
import styles from './Conversation.module.css'
|
||||
|
||||
export default function DmConversation({
|
||||
messages
|
||||
}: {
|
||||
messages: OrbisPostInterface[]
|
||||
}) {
|
||||
const { account } = useOrbis()
|
||||
export default function DmConversation() {
|
||||
const { orbis, account, conversationId, hasLit, connectLit } = useOrbis()
|
||||
const isMounted = useIsMounted()
|
||||
|
||||
const conversationBox = useRef(null)
|
||||
const messagesWrapper = useRef(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [messages, setMessages] = useState<IOrbisMessage[]>([])
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
const [newMessages, setNewMessages] = useState(0)
|
||||
|
||||
const scrollToBottom = (smooth = false) => {
|
||||
setTimeout(() => {
|
||||
messagesWrapper.current.scrollTo({
|
||||
top: messagesWrapper.current.scrollHeight,
|
||||
behavior: smooth ? 'smooth' : 'auto'
|
||||
})
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const getMessages = async (polling = false) => {
|
||||
if (isLoading) return
|
||||
|
||||
if (!polling) setIsLoading(true)
|
||||
|
||||
const _page = polling ? 0 : currentPage
|
||||
const { data, error } = await orbis.getMessages(conversationId, _page)
|
||||
|
||||
if (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
if (data) {
|
||||
data.reverse()
|
||||
if (!polling) {
|
||||
setHasMore(data.length >= 50)
|
||||
const _messages = [...data, ...messages]
|
||||
setMessages(_messages)
|
||||
if (currentPage === 0) scrollToBottom()
|
||||
setCurrentPage((prev) => prev + 1)
|
||||
} else {
|
||||
const unique = data.filter(
|
||||
(a) => !messages.some((b) => a.stream_id === b.stream_id)
|
||||
)
|
||||
setNewMessages(unique.length)
|
||||
setMessages([...messages, ...unique])
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
useInterval(async () => {
|
||||
await getMessages(true)
|
||||
}, 7000)
|
||||
|
||||
const showTime = (index: number): boolean => {
|
||||
const nextMessage = messages[index + 1]
|
||||
@ -21,43 +73,104 @@ export default function DmConversation({
|
||||
return nextMessage.timestamp - messages[index].timestamp > 60
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log(messages)
|
||||
if (messages.length && conversationBox) {
|
||||
setTimeout(() => {
|
||||
console.log(
|
||||
conversationBox.current.scrollTop,
|
||||
conversationBox.current.scrollHeight
|
||||
)
|
||||
conversationBox.current.scrollTop = conversationBox.current.scrollHeight
|
||||
}, 100)
|
||||
const callback = (nMessage: IOrbisMessage) => {
|
||||
const _messages = [...messages, nMessage]
|
||||
setMessages(_messages)
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
const onScrollMessages = throttle(() => {
|
||||
const el = messagesWrapper.current
|
||||
|
||||
if (hasMore && el.scrollTop === 0) {
|
||||
getMessages()
|
||||
}
|
||||
}, [messages, conversationBox])
|
||||
|
||||
if (
|
||||
Math.ceil(el.scrollTop) >= Math.floor(el.scrollHeight - el.offsetHeight)
|
||||
) {
|
||||
setNewMessages(0)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
useEffect(() => {
|
||||
if (isMounted) {
|
||||
if (conversationId && orbis) {
|
||||
getMessages()
|
||||
} else {
|
||||
setMessages([])
|
||||
}
|
||||
}
|
||||
}, [conversationId, orbis, isMounted])
|
||||
|
||||
useEffect(() => {
|
||||
const el = messagesWrapper.current
|
||||
|
||||
el.addEventListener('scroll', onScrollMessages)
|
||||
|
||||
return () => {
|
||||
el.removeEventListener('scroll', onScrollMessages)
|
||||
}
|
||||
}, [messagesWrapper])
|
||||
|
||||
return (
|
||||
<div ref={conversationBox} className={styles.conversationBox}>
|
||||
{messages.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`${styles.message} ${
|
||||
account?.did === message.creator_details.did
|
||||
? styles.right
|
||||
: styles.left
|
||||
} ${showTime(index) && styles.showTime}`}
|
||||
>
|
||||
<div className={styles.chatBubble}>
|
||||
<DecryptedMessage content={message.content} />
|
||||
</div>
|
||||
<div className={styles.time}>
|
||||
<Time
|
||||
date={message.timestamp.toString()}
|
||||
isUnix={true}
|
||||
relative={false}
|
||||
displayFormat="MMM d, yyyy, h:mm aa"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.conversation}>
|
||||
{!hasLit ? (
|
||||
<div className={styles.connectLit}>
|
||||
<p>
|
||||
You need to configure your private account to access your private
|
||||
conversations.
|
||||
</p>
|
||||
<Button
|
||||
style="primary"
|
||||
size="small"
|
||||
disabled={false}
|
||||
onClick={() => connectLit()}
|
||||
>
|
||||
Generate private account
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
) : (
|
||||
<>
|
||||
{isLoading && <div className={styles.loading}>Loading...</div>}
|
||||
<div className={styles.messages}>
|
||||
<div ref={messagesWrapper} className={styles.scrollContent}>
|
||||
{messages.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`${styles.message} ${
|
||||
account?.did === message.creator_details.did
|
||||
? styles.right
|
||||
: styles.left
|
||||
} ${showTime(index) && styles.showTime}`}
|
||||
>
|
||||
<div className={styles.chatBubble}>
|
||||
<DecryptedMessage content={message.content} />
|
||||
</div>
|
||||
<div className={styles.time}>
|
||||
<Time
|
||||
date={message.timestamp.toString()}
|
||||
isUnix={true}
|
||||
relative={false}
|
||||
displayFormat="MMM d, yyyy, h:mm aa"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{newMessages > 0 && (
|
||||
<button
|
||||
className={styles.newMessagesBadge}
|
||||
onClick={() => scrollToBottom(true)}
|
||||
>
|
||||
{newMessages} new {newMessages > 1 ? 'messages' : 'message'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Postbox conversationId={conversationId} callback={callback} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -5,28 +5,30 @@ import styles from './DecryptedMessage.module.css'
|
||||
export default function DecryptedMessage({
|
||||
content
|
||||
}: {
|
||||
content: OrbisPostContentInterface
|
||||
content: IOrbisMessageContent & { decryptedMessage?: string }
|
||||
}) {
|
||||
const { orbis } = useOrbis()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [decrypted, setDecrypted] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
const decryptMessage = async (content: OrbisPostContentInterface) => {
|
||||
if (!content?.encryptedMessage) {
|
||||
setLoading(false)
|
||||
setDecrypted(content.body)
|
||||
return
|
||||
}
|
||||
const decryptMessage = async () => {
|
||||
setLoading(true)
|
||||
const res = await orbis.decryptMessage(content)
|
||||
|
||||
setDecrypted(res.result)
|
||||
if (content?.decryptedMessage) {
|
||||
setDecrypted(content?.decryptedMessage)
|
||||
} else {
|
||||
const res = await orbis.decryptMessage({
|
||||
conversation_id: content?.conversation_id,
|
||||
encryptedMessage: content?.encryptedMessage
|
||||
})
|
||||
setDecrypted(res.result)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
if (content && orbis) {
|
||||
console.log(content)
|
||||
decryptMessage(content)
|
||||
decryptMessage()
|
||||
}
|
||||
}, [content, orbis])
|
||||
|
||||
|
@ -0,0 +1,66 @@
|
||||
.header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: calc(0.35 * var(--spacer)) var(--spacer);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
box-shadow: var(--box-shadow);
|
||||
font-size: var(--font-size-h5);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 0;
|
||||
width: 1.5rem;
|
||||
margin-right: calc(0.5 * var(--spacer));
|
||||
fill: var(--font-color-text);
|
||||
}
|
||||
|
||||
.back {
|
||||
margin-left: 0;
|
||||
width: 1rem;
|
||||
margin-right: calc(0.5 * var(--spacer));
|
||||
fill: var(--font-color-text);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.btnBack {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
cursor: pointer;
|
||||
margin-left: auto;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--font-color-text);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.toggle .icon {
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
.isFlipped {
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
|
||||
.notificationCount {
|
||||
background-color: var(--color-primary);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--brand-white);
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-mini);
|
||||
font-weight: 600;
|
||||
margin-left: calc(var(--spacer) / 4);
|
||||
}
|
58
src/components/@shared/Orbis/DirectMessages/Header.tsx
Normal file
58
src/components/@shared/Orbis/DirectMessages/Header.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import React from 'react'
|
||||
import styles from './Header.module.css'
|
||||
import { useOrbis } from '@context/Orbis'
|
||||
import ChatBubble from '@images/chatbubble.svg'
|
||||
import ArrowBack from '@images/arrow.svg'
|
||||
import ChevronUp from '@images/chevronup.svg'
|
||||
|
||||
export default function Header() {
|
||||
const {
|
||||
// unreadMessages,
|
||||
conversationId,
|
||||
openConversations,
|
||||
conversationTitle,
|
||||
setOpenConversations,
|
||||
setConversationId
|
||||
} = useOrbis()
|
||||
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
{!conversationId ? (
|
||||
<>
|
||||
<ChatBubble role="img" aria-label="Chat" className={styles.icon} />
|
||||
<span>Direct Messages</span>
|
||||
{/* {unreadMessages.length > 0 && (
|
||||
<span className={styles.notificationCount}>
|
||||
{unreadMessages.length}
|
||||
</span>
|
||||
)} */}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="button"
|
||||
className={styles.btnBack}
|
||||
onClick={() => setConversationId(null)}
|
||||
>
|
||||
<ArrowBack role="img" aria-label="arrow" className={styles.back} />
|
||||
</button>
|
||||
<span>{conversationTitle}</span>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.toggle}
|
||||
onClick={() => setOpenConversations(!openConversations)}
|
||||
>
|
||||
<ChevronUp
|
||||
role="img"
|
||||
aria-label="Toggle"
|
||||
className={`${styles.icon} ${
|
||||
openConversations ? styles.isFlipped : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
20
src/components/@shared/Orbis/DirectMessages/List.module.css
Normal file
20
src/components/@shared/Orbis/DirectMessages/List.module.css
Normal file
@ -0,0 +1,20 @@
|
||||
.conversations {
|
||||
height: 100%;
|
||||
background-color: var(--background-content);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.conversations .empty {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.conversations .empty .icon {
|
||||
width: var(--font-size-h1);
|
||||
fill: currentColor;
|
||||
margin-bottom: calc(var(--spacer) / 4);
|
||||
}
|
36
src/components/@shared/Orbis/DirectMessages/List.tsx
Normal file
36
src/components/@shared/Orbis/DirectMessages/List.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import { useOrbis } from '@context/Orbis'
|
||||
import ListItem from './ListItem'
|
||||
import ChatBubble from '@images/chatbubble.svg'
|
||||
import styles from './List.module.css'
|
||||
|
||||
export default function List() {
|
||||
const { conversations, unreadMessages, setConversationId } = useOrbis()
|
||||
|
||||
const getConversationUnreads = (conversationId: string) => {
|
||||
const _unreads = unreadMessages.filter(
|
||||
(o) => o.content.conversation_id === conversationId
|
||||
)
|
||||
return _unreads.length
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.conversations}>
|
||||
{conversations.length > 0 ? (
|
||||
conversations.map((conversation: IOrbisConversation, index: number) => (
|
||||
<ListItem
|
||||
key={index}
|
||||
conversation={conversation}
|
||||
unreads={getConversationUnreads(conversation.stream_id)}
|
||||
setConversationId={setConversationId}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.empty}>
|
||||
<ChatBubble role="img" aria-label="Chat" className={styles.icon} />
|
||||
<span>No conversation yet...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
.conversationItem {
|
||||
.conversation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: calc(var(--spacer) / 2) var(--spacer);
|
||||
@ -6,17 +6,15 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.conversation:hover {
|
||||
background: var(--background-highlight);
|
||||
}
|
||||
|
||||
.accountAvatarSet {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.accountAvatarSet .notificationCount {
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
}
|
||||
|
||||
.accountAvatar {
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 100%;
|
||||
@ -28,6 +26,9 @@
|
||||
}
|
||||
|
||||
.notificationCount {
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
background-color: var(--color-primary);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@ -44,22 +45,14 @@
|
||||
}
|
||||
|
||||
.accountInfo {
|
||||
-webkit-box-flex: 1;
|
||||
flex: 1;
|
||||
padding-left: calc(var(--spacer) / 2);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.accountHeading {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.accountName {
|
||||
margin-bottom: calc(var(--spacer) / 6);
|
||||
font-size: var(--font-size-medium);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
@ -70,12 +63,3 @@
|
||||
min-width: calc(var(--spacer) * 1.5);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.accountChat {
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--color-secondary);
|
||||
margin-bottom: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
@ -5,14 +5,14 @@ import { accountTruncate } from '@utils/web3'
|
||||
import { didToAddress } from '@utils/orbis'
|
||||
import Avatar from '@shared/atoms/Avatar'
|
||||
import Time from '@shared/atoms/Time'
|
||||
import styles from './ConversationItem.module.css'
|
||||
import styles from './ListItem.module.css'
|
||||
|
||||
export default function ConversationItem({
|
||||
conversation,
|
||||
unreads,
|
||||
setConversationId
|
||||
}: {
|
||||
conversation: OrbisConversationInterface
|
||||
conversation: IOrbisConversation
|
||||
unreads: number
|
||||
setConversationId: (value: string) => void
|
||||
}) {
|
||||
@ -48,7 +48,7 @@ export default function ConversationItem({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.conversationItem}
|
||||
className={styles.conversation}
|
||||
onClick={() => setConversationId(conversation.stream_id)}
|
||||
>
|
||||
<div className={styles.accountAvatarSet}>
|
||||
@ -58,17 +58,15 @@ export default function ConversationItem({
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.accountInfo}>
|
||||
<div className={styles.accountHeading}>
|
||||
<h3 className={styles.accountName}>{name}</h3>
|
||||
<span className={styles.lastMessageDate}>
|
||||
<Time
|
||||
date={conversation.last_message_timestamp.toString()}
|
||||
isUnix={true}
|
||||
relative
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{/* <p className={styles.accountChat}>{conversation.chat}</p> */}
|
||||
<div className={styles.accountName}>{name}</div>
|
||||
<span className={styles.lastMessageDate}>
|
||||
<Time
|
||||
date={conversation.last_message_timestamp.toString()}
|
||||
isUnix={true}
|
||||
relative={false}
|
||||
displayFormat="Pp"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
@ -0,0 +1,66 @@
|
||||
.postbox {
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: calc(var(--spacer) / 4) calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.postbox .postboxInput {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.postbox .editable {
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
flex-grow: 1;
|
||||
outline: none;
|
||||
max-height: 80px;
|
||||
overflow-y: auto;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.postbox .editable:empty:before {
|
||||
content: attr(data-placeholder);
|
||||
color: var(--color-secondary);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.postbox .editable::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.replyto {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: calc(var(--spacer) / 2);
|
||||
padding: calc(var(--spacer) / 2);
|
||||
font-size: var(--font-size-small);
|
||||
background: var(--background-highlight);
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-top-right-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.replytoDetails {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.replytoCancel {
|
||||
flex-shrink: 0;
|
||||
font-size: var(--font-size-large);
|
||||
line-height: 1;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: currentColor;
|
||||
outline: none;
|
||||
}
|
135
src/components/@shared/Orbis/DirectMessages/Postbox.tsx
Normal file
135
src/components/@shared/Orbis/DirectMessages/Postbox.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import React, { useRef, useState, KeyboardEvent } from 'react'
|
||||
import styles from './Postbox.module.css'
|
||||
import { EmojiClickData } from 'emoji-picker-react'
|
||||
import { useOrbis } from '@context/Orbis'
|
||||
import EmojiPicker from '../EmojiPicker'
|
||||
import { accountTruncate } from '@utils/web3'
|
||||
import { didToAddress } from '@utils/orbis'
|
||||
|
||||
export default function Postbox({
|
||||
conversationId,
|
||||
replyTo = null,
|
||||
cancelReplyTo,
|
||||
callback
|
||||
}: {
|
||||
conversationId: string
|
||||
replyTo?: IOrbisMessage
|
||||
cancelReplyTo?: () => void
|
||||
callback: (value: any) => void
|
||||
}) {
|
||||
const [focusOffset, setFocusOffset] = useState<number | undefined>()
|
||||
const [focusNode, setFocusNode] = useState<Node | undefined>()
|
||||
|
||||
const postBoxArea = useRef(null)
|
||||
const { orbis, account } = useOrbis()
|
||||
|
||||
const saveCaretPos = () => {
|
||||
const sel = document.getSelection()
|
||||
if (sel) {
|
||||
setFocusOffset(sel.focusOffset)
|
||||
setFocusNode(sel.focusNode)
|
||||
}
|
||||
}
|
||||
|
||||
const restoreCaretPos = () => {
|
||||
postBoxArea.current.focus()
|
||||
const sel = document.getSelection()
|
||||
sel.collapse(focusNode, focusOffset)
|
||||
}
|
||||
|
||||
const share = async () => {
|
||||
if (!account) return false
|
||||
|
||||
const body = postBoxArea.current.innerText
|
||||
|
||||
const content: IOrbisMessageContent & { decryptedMessage?: string } = {
|
||||
encryptedMessage: null,
|
||||
decryptedMessage: body,
|
||||
master: replyTo ? replyTo.master || replyTo.stream_id : undefined,
|
||||
reply_to: replyTo ? replyTo.stream_id : undefined
|
||||
}
|
||||
|
||||
const _callbackContent: IOrbisMessage = {
|
||||
conversation_id: conversationId,
|
||||
content,
|
||||
creator: account.did,
|
||||
creator_details: {
|
||||
did: account.did,
|
||||
profile: account.details?.profile,
|
||||
metadata: account.details?.metadata
|
||||
},
|
||||
master: replyTo ? replyTo.master || replyTo.stream_id : null,
|
||||
reply_to: replyTo ? replyTo.stream_id : null,
|
||||
reply_to_creator_details: replyTo ? replyTo.creator_details : null,
|
||||
reply_to_details: replyTo ? replyTo.content : null,
|
||||
stream_id: 'new_post',
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
console.log(_callbackContent)
|
||||
|
||||
if (callback) callback(_callbackContent)
|
||||
postBoxArea.current.innerText = ''
|
||||
|
||||
const res = await orbis.sendMessage({
|
||||
conversation_id: conversationId,
|
||||
body
|
||||
})
|
||||
|
||||
if (res.status === 200) {
|
||||
_callbackContent.stream_id = res.doc
|
||||
if (callback) callback(_callbackContent)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!e.key) return
|
||||
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
// Don't generate a new line
|
||||
e.preventDefault()
|
||||
share()
|
||||
}
|
||||
}
|
||||
|
||||
const onEmojiClick = (emojiData: EmojiClickData) => {
|
||||
if (focusOffset === undefined || focusNode === undefined) {
|
||||
postBoxArea.current.innerText += emojiData.emoji
|
||||
} else {
|
||||
restoreCaretPos()
|
||||
document.execCommand('insertHTML', false, emojiData.emoji)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.postbox}>
|
||||
{replyTo && (
|
||||
<div className={styles.replyto}>
|
||||
<div className={styles.replytoDetails}>
|
||||
Replying to{' '}
|
||||
<strong>
|
||||
{replyTo?.creator_details?.metadata?.ensName ||
|
||||
accountTruncate(didToAddress(replyTo?.creator_details?.did))}
|
||||
</strong>
|
||||
</div>
|
||||
<button className={styles.replytoCancel} onClick={cancelReplyTo}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.postboxInput}>
|
||||
<div
|
||||
id="postbox-area"
|
||||
ref={postBoxArea}
|
||||
className={styles.editable}
|
||||
contentEditable={true}
|
||||
data-placeholder="Type your message here..."
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={saveCaretPos}
|
||||
onMouseUp={saveCaretPos}
|
||||
/>
|
||||
<EmojiPicker onEmojiClick={onEmojiClick} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -29,93 +29,21 @@
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.headerWrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bodyWrapper {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.isClosed {
|
||||
transform: translateY(90%);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: calc(0.35 * var(--spacer)) var(--spacer);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
box-shadow: var(--box-shadow);
|
||||
font-size: var(--font-size-h5);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 0;
|
||||
width: 1.5rem;
|
||||
margin-right: calc(0.5 * var(--spacer));
|
||||
fill: var(--font-color-text);
|
||||
}
|
||||
|
||||
.back {
|
||||
margin-left: 0;
|
||||
width: 1rem;
|
||||
margin-right: calc(0.5 * var(--spacer));
|
||||
fill: var(--font-color-text);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.btnBack {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
cursor: pointer;
|
||||
margin-left: auto;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--font-color-text);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.toggle .icon {
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
.isFlipped {
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
|
||||
.conversations {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.notificationCount {
|
||||
background-color: var(--color-primary);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--brand-white);
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-mini);
|
||||
font-weight: 600;
|
||||
margin-left: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.conversation {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
background-color: var(--background-content);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 42rem) {
|
||||
.wrapper {
|
||||
padding: 0 calc(var(--spacer) / 2);
|
||||
|
@ -1,163 +1,24 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import ChatBubble from '@images/chatbubble.svg'
|
||||
import ArrowBack from '@images/arrow.svg'
|
||||
import ChevronDoubleUp from '@images/chevrondoubleup.svg'
|
||||
import React from 'react'
|
||||
import styles from './index.module.css'
|
||||
import Conversation from './Conversation'
|
||||
import ChatToolbar from './ChatToolbar'
|
||||
import { useOrbis } from '@context/Orbis'
|
||||
import ConversationItem from './ConversationItem'
|
||||
import Header from './Header'
|
||||
import List from './List'
|
||||
|
||||
export default function DirectMessages() {
|
||||
const {
|
||||
orbis,
|
||||
account,
|
||||
convOpen,
|
||||
setConvOpen,
|
||||
conversationId,
|
||||
setConversationId,
|
||||
conversations,
|
||||
conversationTitle
|
||||
} = useOrbis()
|
||||
|
||||
const [messages, setMessages] = useState<OrbisPostInterface[]>([])
|
||||
const [unreads, setUnreads] = useState([])
|
||||
|
||||
const getConversationUnreads = (conversationId: string) => {
|
||||
const _unreads = unreads.filter(
|
||||
(o) => o.content.conversation_id === conversationId
|
||||
)
|
||||
return _unreads.length
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const getNotifications = async () => {
|
||||
const { data, error } = await orbis.api.rpc('orbis_f_notifications', {
|
||||
user_did: account?.did || 'none',
|
||||
notif_type: 'messages'
|
||||
})
|
||||
|
||||
if (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
if (data.length > 0) {
|
||||
const _unreads = data.filter((o: OrbisNotificationInterface) => {
|
||||
return o.status === 'new'
|
||||
})
|
||||
setUnreads(_unreads)
|
||||
}
|
||||
}
|
||||
|
||||
if (orbis && account) {
|
||||
getNotifications()
|
||||
}
|
||||
}, [orbis, account])
|
||||
|
||||
const callbackMessage = (nMessage: OrbisPostInterface) => {
|
||||
if (nMessage.stream_id) {
|
||||
const _nMessage = messages.findIndex((o) => {
|
||||
return !o.stream_id
|
||||
})
|
||||
console.log(_nMessage)
|
||||
if (_nMessage > -1) {
|
||||
const _messages = [...messages]
|
||||
_messages[_nMessage] = nMessage
|
||||
setMessages(_messages)
|
||||
}
|
||||
} else {
|
||||
const _messages = [...messages, nMessage]
|
||||
setMessages(_messages)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const getMessages = async (id: string) => {
|
||||
const { data, error } = await orbis.getMessages(id)
|
||||
|
||||
if (data) {
|
||||
data.reverse()
|
||||
// const _messages = [...messages, ...data]
|
||||
setMessages(data)
|
||||
}
|
||||
if (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
if (conversationId && orbis) {
|
||||
getMessages(conversationId)
|
||||
} else {
|
||||
setMessages([])
|
||||
}
|
||||
}, [conversationId, orbis])
|
||||
const { openConversations, conversationId } = useOrbis()
|
||||
|
||||
return (
|
||||
<div className={`${styles.wrapper} ${!convOpen && styles.isClosed}`}>
|
||||
<div
|
||||
className={`${styles.wrapper} ${!openConversations && styles.isClosed}`}
|
||||
>
|
||||
<div className={styles.floating}>
|
||||
<div className={styles.header}>
|
||||
<ChatBubble role="img" aria-label="Chat" className={styles.icon} />
|
||||
<span>Direct Messages</span>
|
||||
{unreads.length > 0 && (
|
||||
<span className={styles.notificationCount}>{unreads.length}</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.toggle}
|
||||
onClick={() => setConvOpen(!convOpen)}
|
||||
>
|
||||
<ChevronDoubleUp
|
||||
role="img"
|
||||
aria-label="Toggle"
|
||||
className={`${styles.icon} ${convOpen && styles.isFlipped}`}
|
||||
/>
|
||||
</button>
|
||||
<div className={styles.headerWrapper}>
|
||||
<Header />
|
||||
</div>
|
||||
<div className={styles.conversations}>
|
||||
{conversations &&
|
||||
conversations.map(
|
||||
(conversation: OrbisConversationInterface, index: number) => (
|
||||
<ConversationItem
|
||||
key={index}
|
||||
conversation={conversation}
|
||||
unreads={getConversationUnreads(conversation.stream_id)}
|
||||
setConversationId={setConversationId}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div className={styles.bodyWrapper}>
|
||||
{conversationId ? <Conversation /> : <List />}
|
||||
</div>
|
||||
{conversationId && (
|
||||
<div className={styles.conversation}>
|
||||
<div className={styles.header}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="button"
|
||||
className={styles.btnBack}
|
||||
onClick={() => setConversationId(null)}
|
||||
>
|
||||
<ArrowBack
|
||||
role="img"
|
||||
aria-label="arrow"
|
||||
className={styles.back}
|
||||
/>
|
||||
</button>
|
||||
<span>{conversationTitle}</span>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.toggle}
|
||||
onClick={() => setConvOpen(!convOpen)}
|
||||
>
|
||||
<ChevronDoubleUp
|
||||
role="img"
|
||||
aria-label="Toggle"
|
||||
className={`${styles.icon} ${convOpen && styles.isFlipped}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<Conversation messages={messages} />
|
||||
<ChatToolbar callbackMessage={callbackMessage} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,18 +1,18 @@
|
||||
.emojiToolTip {
|
||||
position: absolute;
|
||||
padding: calc(var(--spacer) / 4.5) calc(var(--spacer) / 3);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: auto !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
font-size: var(--font-size-title);
|
||||
width: var(--font-size-h3);
|
||||
height: var(--font-size-h3);
|
||||
color: var(--font-color-text);
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.icon:hover,
|
||||
.icon:focus {
|
||||
color: var(--color-primary);
|
||||
|
@ -25,6 +25,10 @@ export default function EmojiPicker({
|
||||
lazyLoadEmojis={true}
|
||||
width={322}
|
||||
height={399}
|
||||
skinTonesDisabled={true}
|
||||
previewConfig={{
|
||||
showPreview: false
|
||||
}}
|
||||
/>
|
||||
}
|
||||
trigger="click focus"
|
||||
|
@ -70,7 +70,7 @@ export default function AssetContent({
|
||||
{debug === true && <DebugOutput title="DDO" output={asset} />}
|
||||
</div>
|
||||
|
||||
<Comment asset={asset} />
|
||||
<Comment context={asset?.id} />
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
|
@ -53,8 +53,16 @@
|
||||
bottom: calc(var(--spacer) / 6);
|
||||
}
|
||||
|
||||
.buttonWrap {
|
||||
.dmButton {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
margin-left: auto;
|
||||
padding: calc(var(--spacer) / 4) 0;
|
||||
}
|
||||
|
||||
.dmButton span {
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--color-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import Account from './Account'
|
||||
import styles from './index.module.css'
|
||||
import { useProfile } from '@context/Profile'
|
||||
import { useOrbis } from '@context/Orbis'
|
||||
import { sleep } from '@utils/index'
|
||||
|
||||
const isDescriptionTextClamped = () => {
|
||||
const el = document.getElementById('description')
|
||||
@ -28,69 +27,30 @@ export default function AccountHeader({
|
||||
accountId: string
|
||||
}): ReactElement {
|
||||
const { profile, ownAccount } = useProfile()
|
||||
const { orbis, setConvOpen, setConversationId, conversations } = useOrbis()
|
||||
const {
|
||||
account: orbisAccount,
|
||||
hasLit,
|
||||
createConversation,
|
||||
getDid
|
||||
} = useOrbis()
|
||||
const [isShowMore, setIsShowMore] = useState(false)
|
||||
const [userDid, setUserDid] = useState<string>()
|
||||
const [userDid, setUserDid] = useState<string | undefined>()
|
||||
|
||||
const toogleShowMore = () => {
|
||||
setIsShowMore(!isShowMore)
|
||||
}
|
||||
|
||||
const createConversation = async () => {
|
||||
const res = await orbis.createConversation({
|
||||
recipients: [userDid],
|
||||
context: 'ocean_market'
|
||||
})
|
||||
if (res.status === 200) {
|
||||
console.log(res)
|
||||
const { data } = await orbis.getConversation(res.doc)
|
||||
console.log(data)
|
||||
setConversationId(res.doc)
|
||||
setConvOpen(true)
|
||||
}
|
||||
console.log('clicked')
|
||||
}
|
||||
|
||||
const checkConversation = () => {
|
||||
const filtered = conversations.filter(
|
||||
(conversation: OrbisConversationInterface) => {
|
||||
// console.log(conversation)
|
||||
console.log(userDid)
|
||||
return conversation.recipients.includes(userDid)
|
||||
}
|
||||
)
|
||||
if (!filtered.length && userDid) {
|
||||
createConversation()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const getDid = async () => {
|
||||
const { data, error } = await orbis.getDids(accountId)
|
||||
console.log(data)
|
||||
if (data) {
|
||||
if (data.length > 0) {
|
||||
console.log(data[0].did)
|
||||
setUserDid(data[0].did)
|
||||
} else if (accountId) {
|
||||
console.log(accountId)
|
||||
setUserDid('did:pkh:eip155:1:' + accountId?.toLocaleLowerCase())
|
||||
} else {
|
||||
console.log('try again')
|
||||
await sleep(1000)
|
||||
getDid()
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.log(error)
|
||||
}
|
||||
const getUserDid = async () => {
|
||||
const did = await getDid(accountId)
|
||||
console.log(did)
|
||||
setUserDid(did)
|
||||
}
|
||||
|
||||
if (orbis && accountId) {
|
||||
getDid()
|
||||
if (accountId) {
|
||||
getUserDid()
|
||||
}
|
||||
}, [orbis, accountId])
|
||||
}, [accountId])
|
||||
|
||||
return (
|
||||
<div className={styles.grid}>
|
||||
@ -101,16 +61,19 @@ export default function AccountHeader({
|
||||
|
||||
<div>
|
||||
{!ownAccount && (
|
||||
<div className={styles.buttonWrap}>
|
||||
<div className={styles.dmButton}>
|
||||
<Button
|
||||
style="primary"
|
||||
size="small"
|
||||
className={styles.sendMessage}
|
||||
disabled={!userDid}
|
||||
onClick={checkConversation}
|
||||
disabled={!userDid || !orbisAccount}
|
||||
onClick={() => createConversation(userDid)}
|
||||
>
|
||||
Send Direct Messages
|
||||
Send Direct Message
|
||||
</Button>
|
||||
{userDid !== undefined && userDid === null && (
|
||||
<span>User has no Ceramic Network DID</span>
|
||||
)}
|
||||
{!orbisAccount && <span>Please connect your wallet</span>}
|
||||
</div>
|
||||
)}
|
||||
<Markdown text={profile?.description} className={styles.description} />
|
||||
|
Loading…
x
Reference in New Issue
Block a user