Encrypted Direct Messaging module (#1870)
* add orbis context * new changes * Add orbis floating chat * adding orbis function * load orbis conversations * update message button in profile * minor fix conversation styles * update orbis version * adding details conditional * updating post comment function * Update send message button style * Fix send message button css * udpate orbis * update posts and floating function * Add chatbox emoji * Add Emoji Picker Theme * useDarkMode emoji fix * remove next dynamic on emoji picker * show conversation title from participant * update callbackMessage. * Update Emoji Picker * Add Emoji Picker to Postbox * rename FloatingChat to DirectMessages * create some global orbis components * update package-lock.json * change any type * change Blockie to Avatar component * fix type errors * fix infinite loop when no comment found * Hide send message button on ownAccount profile * Delete unused component * minor changes * update orbis comment and DM components * fix load older messages on DM scroll * fixed orbis createPost * update optional wallet signs * add return value on connect * add padding bottom to compensate DM component * add conditional connect and sign button * update direct message component * update get notifications logic * rerun npm install * rerun npm install * temporary push * rerun npm install * add new custom hooks * run npm install * update flow on address changed * update custom DID string * remove lit auth signatures on resetStates() * add hasLit condition on getMessages * add removeCeramicSession function * useLocalStorage to store notifications * minor bug fix * update styles for conversation details * use getEnsName util * update create conversation flow * rerun npm install * update typescript * update orbis sdk version * temporary push * revisions * update orbis version * update notifs count and conversation creation flow * update orbis types * add toast after copy address * add message decryption refresh button * rerun npm install * remove comment from asset page * test push * remove lit-auth-signature on wallet changed * update orbis SDK to v0.4.14 * update copy * update Orbis SDK to v0.4.17 * update copy * create new DM button component and add to asset * add send button and remove emojiPicker * Revert "Merge branch 'main' into orbis" This reverts commit3cdaf54827
, reversing changes made to02f2acb774
. * Revert "Revert "Merge branch 'main' into orbis"" This reverts commita5a32b1534
. * update new conversation flow * update intro message * minor fix typo * remove unused package and fixed outdated versions * remove comment component and restructured folders * update orbis-sdk * small cleanup * direct message button style updates --------- Co-authored-by: Nuary Pradipta <nuary.pradipta@gmail.com> Co-authored-by: Dollar Bull <ramadhanakhri@gmail.com> Co-authored-by: Bogdan Fazakas <bogdan.fazakas@gmail.com>
4119
package-lock.json
generated
@ -32,6 +32,7 @@
|
||||
"@oceanprotocol/lib": "^2.7.0",
|
||||
"@oceanprotocol/typographies": "^0.1.0",
|
||||
"@oceanprotocol/use-dark-mode": "^2.4.3",
|
||||
"@orbisclub/orbis-sdk": "^0.4.40",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@uiw/react-codemirror": "^4.19.5",
|
||||
"@urql/exchange-refocus": "^1.0.0",
|
||||
@ -62,6 +63,7 @@
|
||||
"react-paginate": "^8.1.4",
|
||||
"react-select": "^5.7.0",
|
||||
"react-spring": "^9.5.5",
|
||||
"react-string-replace": "^1.1.0",
|
||||
"react-tabs": "^6.0.0",
|
||||
"react-toastify": "^9.1.1",
|
||||
"remark": "^14.0.2",
|
||||
|
665
src/@context/DirectMessages/_types.ts
Normal file
@ -0,0 +1,665 @@
|
||||
export interface IOrbisConstructor {
|
||||
ceramic?: unknown
|
||||
node?: string
|
||||
store?: string
|
||||
PINATA_GATEWAY?: string
|
||||
PINATA_API_KEY?: string
|
||||
PINATA_SECRET_API_KEY?: string
|
||||
useLit?: boolean
|
||||
}
|
||||
|
||||
export interface IOrbis {
|
||||
api: any
|
||||
session: any
|
||||
connect: (provider: unknown, lit?: boolean) => Promise<IOrbisConnectReturns>
|
||||
connect_v2: (opts?: {
|
||||
provider?: unknown
|
||||
chain?: string
|
||||
lit?: boolean
|
||||
oauth?: unknown
|
||||
}) => Promise<IOrbisConnectReturns>
|
||||
connectLit: (provider: unknown) => Promise<{
|
||||
status?: number
|
||||
error?: unknown
|
||||
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: unknown,
|
||||
tags: string[],
|
||||
schema: string,
|
||||
family: string
|
||||
) => Promise<{
|
||||
status: number
|
||||
doc?: string
|
||||
error?: unknown
|
||||
result: string
|
||||
}>
|
||||
decryptMessage: (content: {
|
||||
conversation_id: string
|
||||
encryptedMessage: IOrbisEncryptedBody
|
||||
}) => Promise<{
|
||||
result: string
|
||||
status: number
|
||||
}>
|
||||
decryptPost: (content: IOrbisPostContent) => Promise<{ result: string }>
|
||||
deletePost: (stream_id: string) => Promise<{
|
||||
status: number
|
||||
result: string
|
||||
}>
|
||||
deterministicDocument: (
|
||||
content: unknown,
|
||||
tags: string[],
|
||||
schema?: string,
|
||||
family?: string
|
||||
) => Promise<{
|
||||
status: number
|
||||
doc?: string
|
||||
error?: unknown
|
||||
result: string
|
||||
}>
|
||||
editPost: (
|
||||
stream_id: string,
|
||||
content: IOrbisPostContent,
|
||||
encryptionRules?: IOrbisEncryptionRules
|
||||
) => Promise<{
|
||||
status: number
|
||||
result: string
|
||||
}>
|
||||
getChannel: (channel_id: string) => Promise<{
|
||||
data: IOrbisChannel
|
||||
error: unknown
|
||||
status: number
|
||||
}>
|
||||
getConversation: (conversation_id: string) => Promise<{
|
||||
status: number
|
||||
data: IOrbisConversation | null
|
||||
error: unknown
|
||||
}>
|
||||
getConversations: (opts: { did: string; context?: string }) => Promise<{
|
||||
data: IOrbisConversation[]
|
||||
error: unknown
|
||||
status: number
|
||||
}>
|
||||
getCredentials: (did: string) => Promise<{
|
||||
data: {
|
||||
stream_id: string
|
||||
family: string
|
||||
content: unknown
|
||||
issuer: string
|
||||
creator: string
|
||||
subject_id: string
|
||||
type: string
|
||||
}[]
|
||||
error: unknown
|
||||
status: number
|
||||
}>
|
||||
getDids: (address: string) => Promise<{
|
||||
data: {
|
||||
did: string
|
||||
details: Pick<IOrbisProfile, 'did' | 'details'>
|
||||
count_followers: number
|
||||
count_following: number
|
||||
pfp: string
|
||||
}[]
|
||||
error: unknown
|
||||
status: number
|
||||
}>
|
||||
getGroup: (group_id: string) => Promise<{
|
||||
data: IOrbisGroup
|
||||
error: unknown
|
||||
status: number
|
||||
}>
|
||||
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: unknown
|
||||
status: number
|
||||
}>
|
||||
getGroups: () => Promise<{
|
||||
data: IOrbisGroup[]
|
||||
error: unknown
|
||||
status: number
|
||||
}>
|
||||
getIsFollowing: (
|
||||
did_following: string,
|
||||
did_followed: string
|
||||
) => Promise<{
|
||||
data: boolean
|
||||
error: unknown
|
||||
status: number
|
||||
}>
|
||||
getIsGroupMember: (
|
||||
group_id: string,
|
||||
did: string
|
||||
) => Promise<{
|
||||
data: boolean
|
||||
error: unknown
|
||||
status: number
|
||||
}>
|
||||
getMessages: (
|
||||
conversation_id: string,
|
||||
page: number
|
||||
) => Promise<{
|
||||
data: IOrbisMessage[]
|
||||
error: unknown
|
||||
status: number
|
||||
}>
|
||||
getNotifications: (
|
||||
options: {
|
||||
context: string
|
||||
did: string
|
||||
master?: string
|
||||
only_master?: boolean
|
||||
tag?: string
|
||||
algorithm?: string
|
||||
},
|
||||
page: number
|
||||
) => Promise<{
|
||||
data: IOrbisNotification[]
|
||||
error: unknown
|
||||
status: number
|
||||
}>
|
||||
getNotificationsCount: (options: {
|
||||
type: string
|
||||
context?: string
|
||||
conversation_id?: string
|
||||
last_read_timestamp?: number
|
||||
}) => Promise<{
|
||||
data: { count_new_notifications: number }
|
||||
error: unknown
|
||||
status: number
|
||||
}>
|
||||
getPost: (post_id: string) => Promise<{
|
||||
data: IOrbisPost
|
||||
error: unknown
|
||||
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: unknown
|
||||
status: number
|
||||
}>
|
||||
getReaction: (
|
||||
post_id: string,
|
||||
did: string
|
||||
) => Promise<{
|
||||
data: { type: string }
|
||||
error: unknown
|
||||
status: number
|
||||
}>
|
||||
getProfile: (did: string) => Promise<{
|
||||
data: IOrbisProfile
|
||||
error: unknown
|
||||
status: number
|
||||
}>
|
||||
getProfileFollowers: (did: string) => Promise<{
|
||||
data: IOrbisProfile['details'][]
|
||||
error: unknown
|
||||
status: number
|
||||
}>
|
||||
getProfileFollowing: (did: string) => Promise<{
|
||||
data: IOrbisProfile['details'][]
|
||||
error: unknown
|
||||
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: unknown
|
||||
status: number
|
||||
}>
|
||||
getProfilesByUsername: (username: string) => Promise<{
|
||||
data: IOrbisProfile[]
|
||||
error: unknown
|
||||
status: number
|
||||
}>
|
||||
isConnected: (sessionString?: string) => Promise<IOrbisConnectReturns>
|
||||
logout: () => {
|
||||
status: number
|
||||
result: string
|
||||
error: unknown
|
||||
}
|
||||
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?: unknown
|
||||
result: string
|
||||
}>
|
||||
setNotificationsReadTime: (
|
||||
type: string,
|
||||
timestamp: number,
|
||||
context?: string
|
||||
) => Promise<{
|
||||
status: number
|
||||
doc?: string
|
||||
error?: unknown
|
||||
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: unknown,
|
||||
tags: string[],
|
||||
schema: string,
|
||||
family?: string
|
||||
) => Promise<{
|
||||
status: number
|
||||
doc?: string
|
||||
error?: unknown
|
||||
result: string
|
||||
}>
|
||||
uploadMedia: (file: File) => Promise<{
|
||||
status: number
|
||||
error?: unknown
|
||||
result: { url: string; gateway: string } | string
|
||||
}>
|
||||
}
|
||||
|
||||
export interface IOrbisConnectReturns {
|
||||
status: number
|
||||
did: string
|
||||
details: {
|
||||
did: string
|
||||
profile: IOrbisProfile['details']['profile']
|
||||
hasLit: boolean
|
||||
}
|
||||
result: string
|
||||
}
|
||||
|
||||
export enum IOrbisGetPostsAlgorithm {
|
||||
'recommendations',
|
||||
'all-posts',
|
||||
'all-master-posts',
|
||||
'all-did-master-posts',
|
||||
'all-context-master-posts',
|
||||
'all-posts-non-filtered',
|
||||
''
|
||||
}
|
||||
|
||||
export enum OrbisReaction {
|
||||
'like',
|
||||
'haha',
|
||||
'downvote'
|
||||
}
|
||||
|
||||
export 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
|
||||
}
|
||||
|
||||
export 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'
|
||||
}
|
||||
|
||||
export interface IOrbisProfile {
|
||||
address: string
|
||||
count_followers: number
|
||||
count_following: number
|
||||
details: {
|
||||
a_r?: number
|
||||
did: string
|
||||
metadata?: {
|
||||
address?: string
|
||||
chain?: string
|
||||
ensName?: string
|
||||
}
|
||||
nonces?: number
|
||||
profile?: {
|
||||
cover?: string
|
||||
data?: object
|
||||
description?: string
|
||||
pfp?: string
|
||||
pfpIsNft?: {
|
||||
chain: string
|
||||
contract: string
|
||||
timestamp: string
|
||||
tokenId: string
|
||||
}
|
||||
username?: string
|
||||
}
|
||||
twitter_details?: {
|
||||
credential_id: string
|
||||
issuer: string
|
||||
timestamp: number
|
||||
username: string
|
||||
}
|
||||
}
|
||||
did: string
|
||||
last_activity_timestamp: number
|
||||
username: string
|
||||
}
|
||||
|
||||
export interface IOrbisEncryptionRules {
|
||||
type: 'token-gated' | 'custom'
|
||||
chain: string
|
||||
contractType: 'ERC20' | 'ERC721' | 'ERC1155'
|
||||
contractAddress: string
|
||||
minTokenBalance: string
|
||||
tokenId: string
|
||||
accessControlConditions?: object
|
||||
}
|
||||
|
||||
export interface IOrbisEncryptedBody {
|
||||
accessControlConditions: string
|
||||
encryptedString: string
|
||||
encryptedSymmetricKey: string
|
||||
}
|
||||
|
||||
export interface IOrbisPostMention {
|
||||
did: string
|
||||
username: string
|
||||
}
|
||||
|
||||
export interface IOrbisPostContent {
|
||||
body: string
|
||||
title?: string
|
||||
context?: string
|
||||
master?: string
|
||||
mentions?: IOrbisPostMention[]
|
||||
reply_to?: string
|
||||
type?: string
|
||||
tags?: {
|
||||
slug: string
|
||||
title: string
|
||||
}[]
|
||||
data?: object
|
||||
encryptionRules?: IOrbisEncryptionRules | null
|
||||
encryptedMessage?: object | null
|
||||
encryptedBody?: IOrbisEncryptedBody | null
|
||||
}
|
||||
|
||||
export interface IOrbisPost {
|
||||
content: IOrbisPostContent
|
||||
context?: string
|
||||
context_details?: {
|
||||
channel_details?: IOrbisChannel['content']
|
||||
channel_id?: string
|
||||
group_details?: IOrbisGroup['content']
|
||||
group_id?: string
|
||||
}
|
||||
count_commits?: number
|
||||
count_downvotes?: number
|
||||
count_haha?: number
|
||||
count_likes?: number
|
||||
count_replies?: number
|
||||
creator: string
|
||||
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?: Pick<
|
||||
IOrbisProfile['details'],
|
||||
'did' | 'metadata' | 'profile'
|
||||
>
|
||||
reply_to_details?: IOrbisPostContent
|
||||
stream_id: string
|
||||
timestamp: number
|
||||
type?: string
|
||||
}
|
||||
|
||||
export 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
|
||||
}
|
||||
|
||||
export 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
|
||||
}
|
||||
|
||||
export interface IOrbisConversation {
|
||||
content: {
|
||||
recipients: string[]
|
||||
}
|
||||
context: string | null
|
||||
details: {
|
||||
content: {
|
||||
recipients: string[]
|
||||
creator: string
|
||||
}
|
||||
}
|
||||
last_message_timestamp: number
|
||||
last_timestamp_read: number
|
||||
recipients: string[]
|
||||
recipients_details: IOrbisProfile['details'][]
|
||||
stream_id: string
|
||||
}
|
||||
|
||||
export interface IOrbisNotification {
|
||||
content: {
|
||||
conversation_id: string
|
||||
encryptedMessage: IOrbisEncryptedBody
|
||||
}
|
||||
family: string
|
||||
post_details: object
|
||||
status: string
|
||||
type: string
|
||||
user_notifying_details: {
|
||||
did: string
|
||||
profile: IOrbisProfile['details']['profile']
|
||||
}
|
||||
}
|
||||
|
||||
export interface IConversationWithAdditionalData extends IOrbisConversation {
|
||||
notifications_count: number
|
||||
empty_message: boolean
|
||||
}
|
||||
|
||||
export type IOrbisProvider = {
|
||||
orbis: IOrbis
|
||||
account: IOrbisProfile
|
||||
hasLit: boolean
|
||||
openConversations: boolean
|
||||
conversationId: string
|
||||
conversations: IConversationWithAdditionalData[]
|
||||
activeConversationTitle: string
|
||||
notifsLastRead: Record<string, Record<string, number>>
|
||||
totalNotifications: number
|
||||
connectOrbis: (options: {
|
||||
address: string
|
||||
lit?: boolean
|
||||
}) => Promise<IOrbisProfile | null>
|
||||
disconnectOrbis: (address: string) => void
|
||||
checkOrbisConnection: (options: {
|
||||
address: string
|
||||
autoConnect?: boolean
|
||||
lit?: boolean
|
||||
}) => Promise<IOrbisProfile>
|
||||
connectLit: () => Promise<{
|
||||
status?: number
|
||||
error?: unknown
|
||||
result?: string
|
||||
}>
|
||||
setActiveConversationTitle: (title: string) => void
|
||||
setOpenConversations: (open: boolean) => void
|
||||
setConversationId: (conversationId: string) => void
|
||||
getConversationByDid: (userDid: string) => Promise<IOrbisConversation>
|
||||
createConversation: (recipients: string[]) => Promise<string>
|
||||
getConversationTitle: (conversationId: string) => Promise<string>
|
||||
getDid: (address: string) => Promise<string>
|
||||
clearConversationNotifs: (conversationId: string) => void
|
||||
updateConversationEmptyMessageStatus: (
|
||||
conversationId: string,
|
||||
status: boolean
|
||||
) => void
|
||||
}
|
482
src/@context/DirectMessages/index.tsx
Normal file
@ -0,0 +1,482 @@
|
||||
import React, {
|
||||
useContext,
|
||||
createContext,
|
||||
useState,
|
||||
useMemo,
|
||||
useEffect,
|
||||
ReactNode,
|
||||
ReactElement
|
||||
} from 'react'
|
||||
import { useInterval } from '@hooks/useInterval'
|
||||
import { Orbis } from '@orbisclub/orbis-sdk'
|
||||
import { useWeb3 } from '../Web3'
|
||||
import { accountTruncate } from '@utils/web3'
|
||||
import { didToAddress, sleep } from '@shared/DirectMessages/_utils'
|
||||
import { getEnsName } from '@utils/ens'
|
||||
import usePrevious from '@hooks/usePrevious'
|
||||
import useLocalStorage from '@hooks/useLocalStorage'
|
||||
import DirectMessages from '@shared/DirectMessages'
|
||||
import {
|
||||
IOrbis,
|
||||
IOrbisProfile,
|
||||
IOrbisProvider,
|
||||
IConversationWithAdditionalData
|
||||
} from './_types'
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
|
||||
const OrbisContext = createContext({} as IOrbisProvider)
|
||||
|
||||
const orbis: IOrbis = new Orbis()
|
||||
const NOTIFICATION_REFRESH_INTERVAL = 5000
|
||||
const CONVERSATION_CONTEXT =
|
||||
process.env.NEXT_PUBLIC_ORBIS_CONTEXT || 'ocean_market' // Can be changed to whatever
|
||||
|
||||
function OrbisProvider({ children }: { children: ReactNode }): ReactElement {
|
||||
const { web3Provider, accountId } = useWeb3()
|
||||
const prevAccountId = usePrevious(accountId)
|
||||
|
||||
const [ceramicSessions, setCeramicSessions] = useLocalStorage<string[]>(
|
||||
'ocean-ceramic-sessions',
|
||||
[]
|
||||
)
|
||||
const [notifsLastRead, setNotifsLastRead] = useLocalStorage<
|
||||
Record<string, Record<string, number>>
|
||||
>('ocean-notifs-last-read', {})
|
||||
|
||||
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<
|
||||
IConversationWithAdditionalData[]
|
||||
>([])
|
||||
const [activeConversationTitle, setActiveConversationTitle] = useState(null)
|
||||
|
||||
// Function to reset states
|
||||
const resetStates = () => {
|
||||
setAccount(null)
|
||||
setConversationId(null)
|
||||
setConversations([])
|
||||
setHasLit(false)
|
||||
}
|
||||
|
||||
// Remove ceramic session
|
||||
const removeCeramicSession = (address: string) => {
|
||||
const _ceramicSessions = { ...ceramicSessions }
|
||||
delete _ceramicSessions[address.toLowerCase()]
|
||||
setCeramicSessions({ ..._ceramicSessions })
|
||||
}
|
||||
|
||||
// Remove lit signature
|
||||
const removeLitSignature = () => {
|
||||
window.localStorage.removeItem('lit-auth-signature')
|
||||
window.localStorage.removeItem('lit-auth-sol-signature')
|
||||
}
|
||||
|
||||
// Connecting to Orbis
|
||||
const connectOrbis = async ({
|
||||
address,
|
||||
lit = false
|
||||
}: {
|
||||
address: string
|
||||
lit?: boolean
|
||||
}) => {
|
||||
const res = await orbis.connect_v2({
|
||||
provider: web3Provider,
|
||||
chain: 'ethereum',
|
||||
lit
|
||||
})
|
||||
|
||||
if (res.status === 200) {
|
||||
const { data } = await orbis.getProfile(res.did)
|
||||
setAccount(data)
|
||||
setHasLit(res.details.hasLit)
|
||||
const sessionString = orbis.session.serialize()
|
||||
setCeramicSessions({
|
||||
...ceramicSessions,
|
||||
[address.toLowerCase()]: sessionString
|
||||
})
|
||||
return data
|
||||
} else {
|
||||
await connectOrbis({ address })
|
||||
}
|
||||
}
|
||||
|
||||
const disconnectOrbis = (address: string) => {
|
||||
const res = orbis.logout()
|
||||
if (res.status === 200) {
|
||||
resetStates()
|
||||
removeLitSignature()
|
||||
removeCeramicSession(address)
|
||||
}
|
||||
}
|
||||
|
||||
const connectLit = async () => {
|
||||
const res = await orbis.connectLit(web3Provider)
|
||||
setHasLit(res.status === 200)
|
||||
return res
|
||||
}
|
||||
|
||||
const checkOrbisConnection = async ({
|
||||
address,
|
||||
autoConnect,
|
||||
lit
|
||||
}: {
|
||||
address: string
|
||||
autoConnect?: boolean
|
||||
lit?: boolean
|
||||
}) => {
|
||||
const sessionString = ceramicSessions[address.toLowerCase()] || '-'
|
||||
const res = await orbis.isConnected(sessionString)
|
||||
if (
|
||||
res.status === 200 &&
|
||||
didToAddress(res.did) === accountId.toLowerCase()
|
||||
) {
|
||||
setHasLit(res.details.hasLit)
|
||||
const { data } = await orbis.getProfile(res.did)
|
||||
setAccount(data)
|
||||
return data
|
||||
} else if (autoConnect) {
|
||||
const data = await connectOrbis({ address, lit })
|
||||
return data
|
||||
} else {
|
||||
resetStates()
|
||||
removeLitSignature()
|
||||
removeCeramicSession(address)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const getDid = async (address: string) => {
|
||||
if (!address) return null
|
||||
|
||||
const { data, error } = await orbis.getDids(address)
|
||||
|
||||
if (error) {
|
||||
return
|
||||
}
|
||||
|
||||
let _did: string = null
|
||||
|
||||
if (data && data.length > 0) {
|
||||
// Try to get mainnet did
|
||||
const mainnetDid = data.find(
|
||||
(o: {
|
||||
did: string
|
||||
details: Pick<IOrbisProfile, 'did' | 'details'>
|
||||
}) => {
|
||||
const did = o.did.split(':')
|
||||
return did[3] === '1'
|
||||
}
|
||||
)
|
||||
|
||||
_did = mainnetDid?.did || data[0].did
|
||||
} else {
|
||||
_did = `did:pkh:eip155:1:${address.toLowerCase()}`
|
||||
}
|
||||
|
||||
return _did
|
||||
}
|
||||
|
||||
const getConversationNotifications: (
|
||||
conversations: IConversationWithAdditionalData[]
|
||||
) => Promise<void> = async (conversations) => {
|
||||
if (!conversations.length || !orbis) return
|
||||
|
||||
let did = account?.did
|
||||
|
||||
if (!did && accountId) {
|
||||
did = await getDid(accountId)
|
||||
}
|
||||
|
||||
const _newConversations = await Promise.all(
|
||||
conversations.map(async (conversation) => {
|
||||
// Get timestamp of last read notification
|
||||
const lastRead =
|
||||
notifsLastRead[accountId]?.[conversation.stream_id] || 0
|
||||
|
||||
const { data, error } = await orbis.api
|
||||
.rpc('orbis_f_count_notifications_alpha', {
|
||||
user_did: did,
|
||||
notif_type: 'messages',
|
||||
q_context: CONVERSATION_CONTEXT,
|
||||
q_conversation_id: conversation.stream_id,
|
||||
q_last_read: lastRead
|
||||
})
|
||||
.single()
|
||||
|
||||
if (error) {
|
||||
LoggerInstance.error(`[directMessages] orbis api error: `, error)
|
||||
}
|
||||
|
||||
if (data) {
|
||||
const newNotifsCount = data.count_new_notifications
|
||||
// Get conversation by stream_id
|
||||
conversation.notifications_count = newNotifsCount
|
||||
}
|
||||
|
||||
const { data: _data, error: _error } = await orbis.getMessages(
|
||||
conversation.stream_id,
|
||||
0
|
||||
)
|
||||
|
||||
if (_error) {
|
||||
LoggerInstance.error(
|
||||
`[directMessages] orbis getMessages sdk error: `,
|
||||
_error
|
||||
)
|
||||
}
|
||||
|
||||
if (_data) {
|
||||
conversation.empty_message = _data.length === 0
|
||||
}
|
||||
|
||||
return conversation
|
||||
})
|
||||
)
|
||||
|
||||
setConversations(_newConversations)
|
||||
}
|
||||
|
||||
const clearConversationNotifs = async (conversationId: string) => {
|
||||
if (!accountId || !conversationId) return
|
||||
|
||||
const _notifsLastRead = { ...notifsLastRead }
|
||||
|
||||
// Add address if not exists
|
||||
if (!_notifsLastRead[accountId]) {
|
||||
_notifsLastRead[accountId] = {}
|
||||
}
|
||||
|
||||
// Add conversationId if not exists
|
||||
if (!_notifsLastRead[accountId][conversationId]) {
|
||||
_notifsLastRead[accountId][conversationId] = 0
|
||||
}
|
||||
|
||||
// Update last read
|
||||
_notifsLastRead[accountId][conversationId] = Math.floor(Date.now() / 1000)
|
||||
setNotifsLastRead(_notifsLastRead)
|
||||
|
||||
// Set conversation notifications count to 0
|
||||
const _conversations = conversations.map((conversation) => {
|
||||
if (conversation.stream_id === conversationId) {
|
||||
conversation.notifications_count = 0
|
||||
}
|
||||
return conversation
|
||||
})
|
||||
|
||||
setConversations(_conversations)
|
||||
}
|
||||
|
||||
const getConversations = async (did: string = null) => {
|
||||
if (!did) return []
|
||||
|
||||
const { data } = await orbis.getConversations({
|
||||
did,
|
||||
context: CONVERSATION_CONTEXT
|
||||
})
|
||||
|
||||
// Only show conversations with unique recipients
|
||||
const filteredConversations: IConversationWithAdditionalData[] = []
|
||||
data.forEach((conversation: IConversationWithAdditionalData) => {
|
||||
if (conversation.recipients.length === 2) {
|
||||
// Sort recipients by alphabetical order and stringify
|
||||
const sortedRecipients = conversation.recipients.sort()
|
||||
const stringifiedRecipients = sortedRecipients.join(',')
|
||||
|
||||
// Check if conversation already exists based on sorted and stringified recipients
|
||||
const found = filteredConversations.find(
|
||||
(o: IConversationWithAdditionalData) =>
|
||||
o.recipients.length > 1 &&
|
||||
o.recipients.sort().join(',') === stringifiedRecipients
|
||||
)
|
||||
|
||||
if (!found) {
|
||||
filteredConversations.push(conversation)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Also fetch message notifications
|
||||
await getConversationNotifications(filteredConversations)
|
||||
|
||||
setConversations(filteredConversations)
|
||||
return filteredConversations
|
||||
}
|
||||
|
||||
const getConversation = async (conversationId: string) => {
|
||||
if (!conversationId) return null
|
||||
const { data, error } = await orbis.getConversation(conversationId)
|
||||
if (error || !data) {
|
||||
await sleep(2000)
|
||||
await getConversation(conversationId)
|
||||
} else {
|
||||
return data as IConversationWithAdditionalData
|
||||
}
|
||||
}
|
||||
|
||||
const getConversationByDid = async (userDid: string) => {
|
||||
if (!account || !userDid) return null
|
||||
|
||||
// Check from current conversations list
|
||||
if (conversations.length > 0) {
|
||||
const filteredConversations = conversations.filter(
|
||||
(conversation: IConversationWithAdditionalData) => {
|
||||
return (
|
||||
conversation.recipients.length === 2 &&
|
||||
conversation.recipients.includes(userDid)
|
||||
)
|
||||
}
|
||||
)
|
||||
if (filteredConversations.length) return filteredConversations[0]
|
||||
}
|
||||
|
||||
// Refetch conversations
|
||||
const _conversations = await getConversations(account?.did)
|
||||
if (!_conversations.length) return null
|
||||
|
||||
const filteredConversations = _conversations.filter(
|
||||
(conversation: IConversationWithAdditionalData) => {
|
||||
return (
|
||||
conversation.recipients.length === 2 &&
|
||||
conversation.recipients.includes(userDid)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (!filteredConversations.length) return null
|
||||
|
||||
return filteredConversations[0]
|
||||
}
|
||||
|
||||
const createConversation = async (recipients: string[]) => {
|
||||
if (!recipients.length) return null
|
||||
|
||||
const res = await orbis.createConversation({
|
||||
recipients,
|
||||
context: CONVERSATION_CONTEXT
|
||||
})
|
||||
|
||||
if (res.status === 200) {
|
||||
await sleep(2000)
|
||||
const _newConversation = await getConversation(res.doc)
|
||||
if (_newConversation) {
|
||||
_newConversation.notifications_count = 0
|
||||
_newConversation.empty_message = true
|
||||
setConversations([_newConversation, ...conversations])
|
||||
return _newConversation.stream_id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getConversationTitle = async (conversationId: string) => {
|
||||
if (conversationId && conversations.length) {
|
||||
// Get conversation based on conversationId
|
||||
const conversation = conversations.find(
|
||||
(o) => o.stream_id === conversationId
|
||||
)
|
||||
|
||||
if (!conversation) return null
|
||||
|
||||
// Get address from did
|
||||
const did = conversation.recipients.find((o: string) => o !== account.did)
|
||||
const address = didToAddress(did)
|
||||
|
||||
// Get ens name if exists
|
||||
const ensName = await getEnsName(address)
|
||||
|
||||
return ensName || accountTruncate(address)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const totalNotifications = useMemo(() => {
|
||||
if (!conversations.length) return 0
|
||||
|
||||
// Loop through conversations and count notifications
|
||||
let count = 0
|
||||
conversations.forEach((conversation: IConversationWithAdditionalData) => {
|
||||
count += conversation?.notifications_count || 0
|
||||
})
|
||||
|
||||
return count
|
||||
}, [conversations])
|
||||
|
||||
const updateConversationEmptyMessageStatus = async (
|
||||
conversationId: string,
|
||||
empty: boolean
|
||||
) => {
|
||||
if (!conversationId) return null
|
||||
|
||||
const _conversations = conversations.map((conversation) => {
|
||||
if (conversation.stream_id === conversationId) {
|
||||
conversation.empty_message = empty
|
||||
}
|
||||
return conversation
|
||||
})
|
||||
|
||||
setConversations(_conversations)
|
||||
}
|
||||
|
||||
useInterval(async () => {
|
||||
await getConversations(account?.did)
|
||||
}, NOTIFICATION_REFRESH_INTERVAL)
|
||||
|
||||
useEffect(() => {
|
||||
if (web3Provider && accountId) {
|
||||
if (accountId !== prevAccountId) {
|
||||
resetStates()
|
||||
removeLitSignature()
|
||||
}
|
||||
// Check if wallet connected
|
||||
checkOrbisConnection({ address: accountId })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [accountId, prevAccountId, web3Provider])
|
||||
|
||||
useEffect(() => {
|
||||
if (account) {
|
||||
getConversations(account?.did)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [account])
|
||||
|
||||
return (
|
||||
<OrbisContext.Provider
|
||||
value={{
|
||||
orbis,
|
||||
account,
|
||||
hasLit,
|
||||
openConversations,
|
||||
conversationId,
|
||||
conversations,
|
||||
activeConversationTitle,
|
||||
notifsLastRead,
|
||||
totalNotifications,
|
||||
connectOrbis,
|
||||
disconnectOrbis,
|
||||
checkOrbisConnection,
|
||||
connectLit,
|
||||
setActiveConversationTitle,
|
||||
setOpenConversations,
|
||||
setConversationId,
|
||||
getConversationByDid,
|
||||
createConversation,
|
||||
getConversationTitle,
|
||||
getDid,
|
||||
clearConversationNotifs,
|
||||
updateConversationEmptyMessageStatus
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<DirectMessages />
|
||||
</OrbisContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useOrbis = () => {
|
||||
return useContext(OrbisContext)
|
||||
}
|
||||
|
||||
export { OrbisProvider, useOrbis }
|
@ -43,7 +43,7 @@ interface Web3ProviderValue {
|
||||
web3Loading: boolean
|
||||
isSupportedOceanNetwork: boolean
|
||||
approvedBaseTokens: TokenInfo[]
|
||||
connect: () => Promise<void>
|
||||
connect: () => Promise<string>
|
||||
logout: () => Promise<void>
|
||||
}
|
||||
|
||||
@ -136,8 +136,10 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
const accountId = (await web3.eth.getAccounts())[0]
|
||||
setAccountId(accountId)
|
||||
LoggerInstance.log('[web3] account id', accountId)
|
||||
return accountId
|
||||
} catch (error) {
|
||||
LoggerInstance.error('[web3] Error: ', error.message)
|
||||
return null
|
||||
} finally {
|
||||
setWeb3Loading(false)
|
||||
}
|
||||
|
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])
|
||||
}
|
43
src/@hooks/useLocalStorage.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import { useState } from 'react'
|
||||
|
||||
function useLocalStorage<T>(key: string, initialValue: T) {
|
||||
// State to store our value
|
||||
// Pass initial state function to useState so logic is only executed once
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return initialValue
|
||||
}
|
||||
try {
|
||||
// Get from local storage by key
|
||||
const item = window.localStorage.getItem(key)
|
||||
// Parse stored json or if none return initialValue
|
||||
return item ? JSON.parse(item) : initialValue
|
||||
} catch (error) {
|
||||
// If error also return initialValue
|
||||
LoggerInstance.error(`[useLocalStorage] error: `, error)
|
||||
return initialValue
|
||||
}
|
||||
})
|
||||
// Return a wrapped version of useState's setter function that ...
|
||||
// ... persists the new value to localStorage.
|
||||
const setValue = (value: T | ((val: T) => T)) => {
|
||||
try {
|
||||
// Allow value to be a function so we have same API as useState
|
||||
const valueToStore =
|
||||
value instanceof Function ? value(storedValue) : value
|
||||
// Save state
|
||||
setStoredValue(valueToStore)
|
||||
// Save to local storage
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore))
|
||||
}
|
||||
} catch (error) {
|
||||
// A more advanced implementation would handle the error case
|
||||
LoggerInstance.error(`[useLocalStorage] error: `, error)
|
||||
}
|
||||
}
|
||||
return [storedValue, setValue] as const
|
||||
}
|
||||
|
||||
export default useLocalStorage
|
15
src/@hooks/usePrevious.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
function usePrevious<T>(value: T): T {
|
||||
// The ref object is a generic container whose current property is mutable ...
|
||||
// ... and can hold any value, similar to an instance property on a class
|
||||
const ref: any = useRef<T>()
|
||||
// Store current value in ref
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
}, [value]) // Only re-run if value changes
|
||||
// Return previous value (happens before update in useEffect above)
|
||||
return ref.current
|
||||
}
|
||||
|
||||
export default usePrevious
|
1
src/@images/chatbubble.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="19.2" height="19.2" viewBox="-1.6 -1.6 19.2 19.2"><path d="M16 8c0 3.866-3.582 7-8 7a9.06 9.06 0 01-2.347-.306c-.584.296-1.925.864-4.181 1.234-.2.032-.352-.176-.273-.362.354-.836.674-1.95.77-2.966C.744 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7zM5 8a1 1 0 10-2 0 1 1 0 002 0zm4 0a1 1 0 10-2 0 1 1 0 002 0zm3 1a1 1 0 100-2 1 1 0 000 2z" /></svg>
|
After Width: | Height: | Size: 399 B |
4
src/@images/chevronup.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" width="24" height="24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 232 B |
2
src/@images/comment.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<svg width="19.2" height="19.2" viewBox="-1.6 -1.6 19.2 19.2"><path fill-rule="evenodd" d="M1.5 2.75a.25.25 0 01.25-.25h8.5a.25.25 0 01.25.25v5.5a.25.25 0 01-.25.25h-3.5a.75.75 0 00-.53.22L3.5 11.44V9.25a.75.75 0 00-.75-.75h-1a.25.25 0 01-.25-.25v-5.5zM1.75 1A1.75 1.75 0 000 2.75v5.5C0 9.216.784 10 1.75 10H2v1.543a1.457 1.457 0 002.487 1.03L7.061 10h3.189A1.75 1.75 0 0012 8.25v-5.5A1.75 1.75 0 0010.25 1h-8.5zM14.5 4.75a.25.25 0 00-.25-.25h-.5a.75.75 0 110-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0114.25 12H14v1.543a1.457 1.457 0 01-2.487 1.03L9.22 12.28a.75.75 0 111.06-1.06l2.22 2.22v-2.19a.75.75 0 01.75-.75h1a.25.25 0 00.25-.25v-5.5z"></path></svg>
|
||||
|
After Width: | Height: | Size: 667 B |
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 |
3
src/@images/emoji.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 1.332C11.6827 1.332 14.668 4.31733 14.668 8C14.668 11.682 11.6827 14.6667 8 14.6667C4.31733 14.6673 1.332 11.682 1.332 8C1.332 4.31733 4.31733 1.332 8 1.332ZM8 2.332C7.25054 2.32387 6.50691 2.46447 5.81215 2.74565C5.11738 3.02683 4.48529 3.44301 3.95244 3.97011C3.4196 4.49721 2.99658 5.12476 2.70788 5.81643C2.41918 6.5081 2.27053 7.25016 2.27053 7.99967C2.27053 8.74917 2.41918 9.49124 2.70788 10.1829C2.99658 10.8746 3.4196 11.5021 3.95244 12.0292C4.48529 12.5563 5.11738 12.9725 5.81215 13.2537C6.50691 13.5349 7.25054 13.6755 8 13.6673C9.49251 13.6511 10.9184 13.0469 11.9681 11.9858C13.0178 10.9246 13.6065 9.49227 13.6065 7.99967C13.6065 6.50707 13.0178 5.07471 11.9681 4.01358C10.9184 2.95244 9.49251 2.34819 8 2.332V2.332ZM5.64133 9.85533C5.92193 10.2125 6.28011 10.5012 6.68875 10.6995C7.09739 10.8978 7.54578 11.0006 8 11C8.45373 11.0006 8.90164 10.898 9.30991 10.7C9.71817 10.5021 10.0761 10.2139 10.3567 9.85733C10.3974 9.80581 10.4478 9.76281 10.5052 9.73079C10.5625 9.69877 10.6256 9.67836 10.6908 9.67071C10.756 9.66306 10.8221 9.66834 10.8853 9.68623C10.9484 9.70413 11.0075 9.73429 11.059 9.775C11.1105 9.81571 11.1535 9.86617 11.1855 9.9235C11.2176 9.98082 11.238 10.0439 11.2456 10.1091C11.2533 10.1743 11.248 10.2404 11.2301 10.3036C11.2122 10.3668 11.182 10.4258 11.1413 10.4773C10.7672 10.9524 10.29 11.3363 9.74583 11.6C9.20167 11.8638 8.60472 12.0006 8 12C7.3945 12.0005 6.79682 11.8633 6.25214 11.5988C5.70747 11.3343 5.23005 10.9495 4.856 10.4733C4.81362 10.422 4.78193 10.3626 4.7628 10.2988C4.74367 10.235 4.73747 10.168 4.74458 10.1018C4.75169 10.0356 4.77196 9.97146 4.8042 9.91318C4.83644 9.8549 4.88 9.80364 4.93232 9.76243C4.98464 9.72121 5.04466 9.69086 5.10887 9.67316C5.17309 9.65547 5.24018 9.65078 5.30623 9.65937C5.37228 9.66796 5.43595 9.68966 5.49349 9.7232C5.55104 9.75674 5.6013 9.80144 5.64133 9.85467V9.85533ZM6 5.83333C6.11137 5.83018 6.22225 5.8494 6.32607 5.88984C6.42989 5.93029 6.52454 5.99114 6.60443 6.0688C6.68432 6.14647 6.74783 6.23936 6.79119 6.342C6.83456 6.44463 6.8569 6.55492 6.8569 6.66633C6.8569 6.77775 6.83456 6.88804 6.79119 6.99067C6.74783 7.09331 6.68432 7.1862 6.60443 7.26386C6.52454 7.34153 6.42989 7.40238 6.32607 7.44283C6.22225 7.48327 6.11137 7.50248 6 7.49933C5.78315 7.4932 5.57725 7.40275 5.42604 7.2472C5.27483 7.09165 5.19024 6.88327 5.19024 6.66633C5.19024 6.4494 5.27483 6.24102 5.42604 6.08547C5.57725 5.92992 5.78315 5.83947 6 5.83333ZM10 5.83333C10.1114 5.83018 10.2222 5.8494 10.3261 5.88984C10.4299 5.93029 10.5245 5.99114 10.6044 6.0688C10.6843 6.14647 10.7478 6.23936 10.7912 6.342C10.8346 6.44463 10.8569 6.55492 10.8569 6.66633C10.8569 6.77775 10.8346 6.88804 10.7912 6.99067C10.7478 7.09331 10.6843 7.1862 10.6044 7.26386C10.5245 7.34153 10.4299 7.40238 10.3261 7.44283C10.2222 7.48327 10.1114 7.50248 10 7.49933C9.78315 7.4932 9.57725 7.40275 9.42604 7.2472C9.27483 7.09165 9.19024 6.88327 9.19024 6.66633C9.19024 6.4494 9.27483 6.24102 9.42604 6.08547C9.57725 5.92992 9.78315 5.83947 10 5.83333V5.83333Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
1
src/@images/laugh.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="24" height="24" viewBox="-51.52 -43.52 599.04 599.04"><path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm141.4 389.4c-37.8 37.8-88 58.6-141.4 58.6s-103.6-20.8-141.4-58.6S48 309.4 48 256s20.8-103.6 58.6-141.4S194.6 56 248 56s103.6 20.8 141.4 58.6S448 202.6 448 256s-20.8 103.6-58.6 141.4zM343.6 196l33.6-40.3c8.6-10.3-3.8-24.8-15.4-18l-80 48c-7.8 4.7-7.8 15.9 0 20.6l80 48c11.5 6.8 24-7.6 15.4-18L343.6 196zm-209.4 58.3l80-48c7.8-4.7 7.8-15.9 0-20.6l-80-48c-11.6-6.9-24 7.7-15.4 18l33.6 40.3-33.6 40.3c-8.7 10.4 3.8 24.8 15.4 18zM362.4 288H133.6c-8.2 0-14.5 7-13.5 15 7.5 59.2 58.9 105 121.1 105h13.6c62.2 0 113.6-45.8 121.1-105 1-8-5.3-15-13.5-15z"></path></svg>
|
After Width: | Height: | Size: 703 B |
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 |
3
src/@images/send.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.17699 1.119C1.25168 1.05563 1.34333 1.01559 1.44058 1.00386C1.53782 0.992127 1.63637 1.00921 1.72399 1.053L14.724 7.553C14.8069 7.59457 14.8767 7.6584 14.9254 7.73734C14.9741 7.81628 15 7.90723 15 8C15 8.09278 14.9741 8.18372 14.9254 8.26266C14.8767 8.34161 14.8069 8.40543 14.724 8.447L1.72399 14.947C1.63643 14.9909 1.5379 15.0081 1.44065 14.9965C1.34339 14.9849 1.25168 14.945 1.17691 14.8817C1.10213 14.8185 1.04759 14.7346 1.02005 14.6406C0.992509 14.5466 0.993183 14.4466 1.02199 14.353L2.97699 8L1.02199 1.647C0.993347 1.55348 0.992767 1.45361 1.02032 1.35976C1.04787 1.26591 1.10234 1.1822 1.17699 1.119V1.119ZM3.86899 8.5L2.32199 13.53L13.382 8L2.32199 2.47L3.86899 7.5H9.49999C9.63259 7.5 9.75977 7.55268 9.85354 7.64645C9.94731 7.74022 9.99999 7.86739 9.99999 8C9.99999 8.13261 9.94731 8.25979 9.85354 8.35355C9.75977 8.44732 9.63259 8.5 9.49999 8.5H3.86999H3.86899Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 996 B |
1
src/@images/thumbsdown.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="24" height="24" viewBox="-43.52 -43.52 599.04 599.04"><path d="M466.27 225.31c4.674-22.647.864-44.538-8.99-62.99 2.958-23.868-4.021-48.565-17.34-66.99C438.986 39.423 404.117 0 327 0c-7 0-15 .01-22.22.01C201.195.01 168.997 40 128 40h-10.845c-5.64-4.975-13.042-8-21.155-8H32C14.327 32 0 46.327 0 64v240c0 17.673 14.327 32 32 32h64c11.842 0 22.175-6.438 27.708-16h7.052c19.146 16.953 46.013 60.653 68.76 83.4 13.667 13.667 10.153 108.6 71.76 108.6 57.58 0 95.27-31.936 95.27-104.73 0-18.41-3.93-33.73-8.85-46.54h36.48c48.602 0 85.82-41.565 85.82-85.58 0-19.15-4.96-34.99-13.73-49.84zM64 296c-13.255 0-24-10.745-24-24s10.745-24 24-24 24 10.745 24 24-10.745 24-24 24zm330.18 16.73H290.19c0 37.82 28.36 55.37 28.36 94.54 0 23.75 0 56.73-47.27 56.73-18.91-18.91-9.46-66.18-37.82-94.54C206.9 342.89 167.28 272 138.92 272H128V85.83c53.611 0 100.001-37.82 171.64-37.82h37.82c35.512 0 60.82 17.12 53.12 65.9 15.2 8.16 26.5 36.44 13.94 57.57 21.581 20.384 18.699 51.065 5.21 65.62 9.45 0 22.36 18.91 22.27 37.81-.09 18.91-16.71 37.82-37.82 37.82z"></path></svg>
|
After Width: | Height: | Size: 1.0 KiB |
1
src/@images/thumbsup.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="24" height="24" viewBox="-43.52 -43.52 599.04 599.04"><path d="M466.27 286.69C475.04 271.84 480 256 480 236.85c0-44.015-37.218-85.58-85.82-85.58H357.7c4.92-12.81 8.85-28.13 8.85-46.54C366.55 31.936 328.86 0 271.28 0c-61.607 0-58.093 94.933-71.76 108.6-22.747 22.747-49.615 66.447-68.76 83.4H32c-17.673 0-32 14.327-32 32v240c0 17.673 14.327 32 32 32h64c14.893 0 27.408-10.174 30.978-23.95 44.509 1.001 75.06 39.94 177.802 39.94 7.22 0 15.22.01 22.22.01 77.117 0 111.986-39.423 112.94-95.33 13.319-18.425 20.299-43.122 17.34-66.99 9.854-18.452 13.664-40.343 8.99-62.99zm-61.75 53.83c12.56 21.13 1.26 49.41-13.94 57.57 7.7 48.78-17.608 65.9-53.12 65.9h-37.82c-71.639 0-118.029-37.82-171.64-37.82V240h10.92c28.36 0 67.98-70.89 94.54-97.46 28.36-28.36 18.91-75.63 37.82-94.54 47.27 0 47.27 32.98 47.27 56.73 0 39.17-28.36 56.72-28.36 94.54h103.99c21.11 0 37.73 18.91 37.82 37.82.09 18.9-12.82 37.81-22.27 37.81 13.489 14.555 16.371 45.236-5.21 65.62zM88 432c0 13.255-10.745 24-24 24s-24-10.745-24-24 10.745-24 24-24 24 10.745 24 24z"></path></svg>
|
After Width: | Height: | Size: 1.0 KiB |
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
|
||||
}
|
138
src/components/@shared/DirectMessages/Conversation.module.css
Normal file
@ -0,0 +1,138 @@
|
||||
.conversation {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--background-content);
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
top: calc(var(--spacer) / 3);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: calc(var(--spacer) / 4);
|
||||
text-align: center;
|
||||
color: var(--color-secondary);
|
||||
/* -webkit-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
-moz-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; */
|
||||
background-color: var(--background-content);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.noMessages {
|
||||
text-align: center;
|
||||
padding: calc(var(--spacer) / 2);
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-bottom: calc(var(--spacer) / 4);
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
.chatBubble {
|
||||
display: inline-block;
|
||||
background-color: var(--color-primary);
|
||||
padding: calc(var(--spacer) / 4) calc(var(--spacer) / 2.5);
|
||||
font-size: var(--font-size-text);
|
||||
border-radius: 1rem;
|
||||
word-break: break-word;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: var(--font-size-mini);
|
||||
color: var(--color-secondary);
|
||||
padding-top: calc(var(--spacer) / 8);
|
||||
}
|
||||
|
||||
.message:not(.showTime) .time {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.message.right {
|
||||
align-items: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.message.right .chatBubble {
|
||||
color: var(--brand-white);
|
||||
background-color: var(--color-primary);
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.message.left .chatBubble {
|
||||
background-color: var(--border-color);
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.message:not(.showTime).right + .message.right .chatBubble {
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.message:not(.showTime).left + .message.left .chatBubble {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
232
src/components/@shared/DirectMessages/Conversation.tsx
Normal file
@ -0,0 +1,232 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { useOrbis } from '@context/DirectMessages'
|
||||
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'
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import { IOrbisMessage } from '@context/DirectMessages/_types'
|
||||
|
||||
export default function DmConversation() {
|
||||
const {
|
||||
orbis,
|
||||
account,
|
||||
conversationId,
|
||||
hasLit,
|
||||
connectLit,
|
||||
clearConversationNotifs
|
||||
} = useOrbis()
|
||||
|
||||
const messagesWrapper = useRef(null)
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [messages, setMessages] = useState<IOrbisMessage[]>([])
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [newMessages, setNewMessages] = useState(0)
|
||||
|
||||
const scrollToBottom = (smooth = false) => {
|
||||
setTimeout(() => {
|
||||
messagesWrapper.current.scrollTo({
|
||||
top: messagesWrapper.current.scrollHeight,
|
||||
behavior: smooth ? 'smooth' : 'auto'
|
||||
})
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const getMessages: (options?: {
|
||||
polling?: boolean
|
||||
reset?: boolean
|
||||
}) => Promise<void> = async ({ polling = false, reset = false }) => {
|
||||
if (isLoading || !hasLit) return
|
||||
|
||||
if (!polling) setIsLoading(true)
|
||||
|
||||
const _page = polling || reset ? 0 : currentPage
|
||||
let _messages = reset ? [] : [...messages]
|
||||
const { data, error } = await orbis.getMessages(conversationId, _page)
|
||||
|
||||
if (error) {
|
||||
LoggerInstance.error(error)
|
||||
}
|
||||
|
||||
if (data.length) {
|
||||
data.reverse()
|
||||
if (!polling) {
|
||||
setHasMore(data.length >= 50)
|
||||
_messages = [...data, ..._messages]
|
||||
setMessages(_messages)
|
||||
if (currentPage === 0) {
|
||||
clearConversationNotifs(conversationId)
|
||||
scrollToBottom()
|
||||
}
|
||||
setCurrentPage(_page + 1)
|
||||
} else {
|
||||
const unique = data.filter(
|
||||
(a) => !_messages.some((b) => a.stream_id === b.stream_id)
|
||||
)
|
||||
setMessages([..._messages, ...unique])
|
||||
const el = messagesWrapper.current
|
||||
if (el && el.scrollHeight > el.offsetHeight) {
|
||||
setNewMessages((prev) => prev + unique.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setIsInitialized(true)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
useInterval(
|
||||
async () => {
|
||||
getMessages({ polling: true })
|
||||
},
|
||||
!isLoading && hasLit && isInitialized ? 5000 : false
|
||||
)
|
||||
|
||||
const showTime = (streamId: string): boolean => {
|
||||
const index = messages.findIndex((o) => o.stream_id === streamId)
|
||||
|
||||
if (index < -1) return true
|
||||
|
||||
const nextMessage = messages[index + 1]
|
||||
if (!nextMessage || messages[index].creator !== nextMessage.creator)
|
||||
return true
|
||||
|
||||
return nextMessage.timestamp - messages[index].timestamp > 60
|
||||
}
|
||||
|
||||
const callback = (nMessage: IOrbisMessage) => {
|
||||
const _messages = [...messages, nMessage]
|
||||
setMessages(_messages)
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
const onScrollMessages = throttle(() => {
|
||||
const el = messagesWrapper.current
|
||||
|
||||
if (!el) return
|
||||
|
||||
if (hasMore && el.scrollTop <= 50) {
|
||||
getMessages()
|
||||
}
|
||||
|
||||
if (
|
||||
Math.ceil(el.scrollTop) >= Math.floor(el.scrollHeight - el.offsetHeight)
|
||||
) {
|
||||
setNewMessages(0)
|
||||
clearConversationNotifs(conversationId)
|
||||
}
|
||||
|
||||
// Remove scroll listener
|
||||
messagesWrapper.current.removeEventListener('scroll', onScrollMessages)
|
||||
|
||||
// Readd scroll listener
|
||||
setTimeout(() => {
|
||||
messagesWrapper.current.addEventListener('scroll', onScrollMessages)
|
||||
}, 100)
|
||||
}, 1000)
|
||||
|
||||
useEffect(() => {
|
||||
setIsInitialized(false)
|
||||
setMessages([])
|
||||
if (
|
||||
conversationId &&
|
||||
!conversationId.startsWith('new-') &&
|
||||
orbis &&
|
||||
hasLit
|
||||
) {
|
||||
getMessages({ reset: true })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [conversationId, orbis, hasLit])
|
||||
|
||||
useEffect(() => {
|
||||
const el = messagesWrapper.current
|
||||
el?.addEventListener('scroll', onScrollMessages)
|
||||
|
||||
return () => {
|
||||
el?.removeEventListener('scroll', onScrollMessages)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [messages])
|
||||
|
||||
return (
|
||||
<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}>
|
||||
{!isLoading && messages.length === 0 ? (
|
||||
<div className={styles.noMessages}>No message yet</div>
|
||||
) : (
|
||||
<div ref={messagesWrapper} className={styles.scrollContent}>
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.stream_id}
|
||||
className={`${styles.message} ${
|
||||
message.stream_id.startsWith('new_post--')
|
||||
? styles.pulse
|
||||
: ''
|
||||
} ${
|
||||
account?.did === message.creator_details.did
|
||||
? styles.right
|
||||
: styles.left
|
||||
} ${showTime(message.stream_id) && styles.showTime}`}
|
||||
>
|
||||
<div className={styles.chatBubble}>
|
||||
<DecryptedMessage
|
||||
content={message.content}
|
||||
position={
|
||||
account?.did === message.creator_details.did
|
||||
? 'left'
|
||||
: 'right'
|
||||
}
|
||||
/>
|
||||
</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 callback={callback} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
.decrypting {
|
||||
-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;
|
||||
}
|
||||
|
||||
.refresh {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
top: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.refreshIcon {
|
||||
fill: var(--font-color-text);
|
||||
}
|
||||
|
||||
.refresh.right {
|
||||
right: -38px;
|
||||
}
|
||||
|
||||
.refresh.left {
|
||||
right: calc(100% + 16px);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
77
src/components/@shared/DirectMessages/DecryptedMessage.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useOrbis } from '@context/DirectMessages'
|
||||
import Refresh from '@images/refresh.svg'
|
||||
import styles from './DecryptedMessage.module.css'
|
||||
import { IOrbisMessageContent } from '@context/DirectMessages/_types'
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
|
||||
export default function DecryptedMessage({
|
||||
content,
|
||||
position = 'right'
|
||||
}: {
|
||||
content: IOrbisMessageContent & { decryptedMessage?: string }
|
||||
position: 'left' | 'right'
|
||||
}) {
|
||||
const { orbis } = useOrbis()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [decrypted, setDecrypted] = useState(null)
|
||||
const [encryptionError, setEncryptionError] = useState<boolean>(false)
|
||||
|
||||
const decryptMessage = async () => {
|
||||
setLoading(true)
|
||||
setEncryptionError(false)
|
||||
|
||||
try {
|
||||
if (content?.decryptedMessage) {
|
||||
setDecrypted(content?.decryptedMessage)
|
||||
} else {
|
||||
const res = await orbis.decryptMessage({
|
||||
conversation_id: content?.conversation_id,
|
||||
encryptedMessage: content?.encryptedMessage
|
||||
})
|
||||
if (res.status === 200) {
|
||||
setEncryptionError(false)
|
||||
setDecrypted(res.result)
|
||||
} else {
|
||||
setEncryptionError(true)
|
||||
setDecrypted('Decryption error - please try later')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
LoggerInstance.error(`[decryptMessage] orbis api error: `, error)
|
||||
setEncryptionError(true)
|
||||
setDecrypted('Decryption error - please try later')
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (content && orbis) decryptMessage()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [content, orbis])
|
||||
|
||||
if (loading) {
|
||||
return <span className={styles.decrypting}>---</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
{!loading ? decrypted : '---'}
|
||||
{encryptionError && (
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.refresh} ${styles[position]}`}
|
||||
onClick={decryptMessage}
|
||||
title="Refresh"
|
||||
>
|
||||
<Refresh
|
||||
role="img"
|
||||
aria-label="Refresh"
|
||||
className={styles.refreshIcon}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
77
src/components/@shared/DirectMessages/DmButton.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Button from '@shared/atoms/Button'
|
||||
import styles from './DmButton.module.css'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
import { useOrbis } from '@context/DirectMessages'
|
||||
|
||||
export default function DmButton({
|
||||
accountId,
|
||||
text = 'Send Message'
|
||||
}: {
|
||||
accountId: string
|
||||
text?: string
|
||||
}) {
|
||||
const { accountId: ownAccountId, connect } = useWeb3()
|
||||
const {
|
||||
checkOrbisConnection,
|
||||
getConversationByDid,
|
||||
setConversationId,
|
||||
setOpenConversations,
|
||||
createConversation,
|
||||
getDid
|
||||
} = useOrbis()
|
||||
const [userDid, setUserDid] = useState<string | undefined>()
|
||||
const [isCreatingConversation, setIsCreatingConversation] = useState(false)
|
||||
|
||||
const handleActivation = async () => {
|
||||
const resConnect = await connect()
|
||||
if (resConnect) {
|
||||
await checkOrbisConnection({
|
||||
address: resConnect,
|
||||
autoConnect: true,
|
||||
lit: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const getUserDid = async () => {
|
||||
const did = await getDid(accountId)
|
||||
setUserDid(did)
|
||||
}
|
||||
|
||||
if (accountId) {
|
||||
getUserDid()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [accountId])
|
||||
|
||||
if (accountId !== ownAccountId && userDid) {
|
||||
return (
|
||||
<Button
|
||||
style="text"
|
||||
size="small"
|
||||
disabled={isCreatingConversation}
|
||||
onClick={async () => {
|
||||
if (!ownAccountId) {
|
||||
handleActivation()
|
||||
} else {
|
||||
setIsCreatingConversation(true)
|
||||
const conversation = await getConversationByDid(userDid)
|
||||
if (conversation) {
|
||||
setConversationId(conversation.stream_id)
|
||||
} else {
|
||||
const newConversationId = await createConversation([userDid])
|
||||
console.log(newConversationId)
|
||||
setConversationId(newConversationId)
|
||||
}
|
||||
setOpenConversations(true)
|
||||
setIsCreatingConversation(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isCreatingConversation ? 'Loading...' : text}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}
|
101
src/components/@shared/DirectMessages/Header.module.css
Normal file
@ -0,0 +1,101 @@
|
||||
.header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: calc(0.5 * var(--spacer));
|
||||
align-items: center;
|
||||
padding: calc(0.35 * var(--spacer)) calc(0.5 * 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);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.5rem;
|
||||
fill: var(--font-color-text);
|
||||
}
|
||||
|
||||
.btnBack {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.btnBack:hover {
|
||||
background-color: var(--background-highlight);
|
||||
}
|
||||
|
||||
.btnBack .backIcon {
|
||||
pointer-events: none;
|
||||
width: 1rem;
|
||||
fill: var(--font-color-text);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.btnCopy {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.btnCopy .copyIcon {
|
||||
pointer-events: none;
|
||||
width: 1rem;
|
||||
fill: var(--color-secondary);
|
||||
}
|
||||
|
||||
.toggleArrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--font-color-text);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.toggleArrow .icon {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
.isFlipped {
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
|
||||
.notificationCount {
|
||||
background-color: var(--color-primary);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
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);
|
||||
}
|
127
src/components/@shared/DirectMessages/Header.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import styles from './Header.module.css'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
import { useOrbis } from '@context/DirectMessages'
|
||||
import { didToAddress } from './_utils'
|
||||
import { toast } from 'react-toastify'
|
||||
import ChatBubble from '@images/chatbubble.svg'
|
||||
import ArrowBack from '@images/arrow.svg'
|
||||
import ChevronUp from '@images/chevronup.svg'
|
||||
import Copy from '@images/copy.svg'
|
||||
|
||||
export default function Header() {
|
||||
const { accountId } = useWeb3()
|
||||
const {
|
||||
conversations,
|
||||
conversationId,
|
||||
openConversations,
|
||||
activeConversationTitle,
|
||||
totalNotifications,
|
||||
setActiveConversationTitle,
|
||||
getConversationTitle,
|
||||
setOpenConversations,
|
||||
setConversationId
|
||||
} = useOrbis()
|
||||
|
||||
const handleClick = (
|
||||
e: React.MouseEvent<HTMLDivElement | HTMLButtonElement>
|
||||
) => {
|
||||
e.preventDefault()
|
||||
const target = e.target as HTMLElement
|
||||
const { role } = target.dataset
|
||||
if (role) {
|
||||
if (role === 'back-button') {
|
||||
setConversationId(null)
|
||||
} else {
|
||||
let _address = ''
|
||||
const conversation = conversations.find(
|
||||
(c) => c.stream_id === conversationId
|
||||
)
|
||||
const recipients = conversation.recipients.filter(
|
||||
(r) => didToAddress(r) !== accountId.toLowerCase()
|
||||
)
|
||||
_address = didToAddress(recipients[0])
|
||||
navigator.clipboard.writeText(_address)
|
||||
toast.info('Address copied to clipboard')
|
||||
}
|
||||
} else {
|
||||
setOpenConversations(!openConversations)
|
||||
}
|
||||
}
|
||||
|
||||
const setConversationTitle = async (conversationId: string) => {
|
||||
if (conversationId.startsWith('new-')) {
|
||||
setActiveConversationTitle(conversationId.replace('new-', ''))
|
||||
} else {
|
||||
const title = await getConversationTitle(conversationId)
|
||||
setActiveConversationTitle(title)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!conversationId) setActiveConversationTitle(null)
|
||||
else setConversationTitle(conversationId)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [conversationId])
|
||||
|
||||
return (
|
||||
<div className={styles.header} onClick={handleClick}>
|
||||
{!conversationId ? (
|
||||
<>
|
||||
<div>
|
||||
<ChatBubble role="img" aria-label="Chat" className={styles.icon} />
|
||||
</div>
|
||||
<span>Direct Messages</span>
|
||||
{totalNotifications > 0 && (
|
||||
<span className={styles.notificationCount}>
|
||||
{totalNotifications}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{openConversations && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="button"
|
||||
data-role="back-button"
|
||||
className={styles.btnBack}
|
||||
>
|
||||
<ArrowBack
|
||||
role="img"
|
||||
aria-label="arrow"
|
||||
className={styles.backIcon}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{activeConversationTitle && (
|
||||
<>
|
||||
<span>{activeConversationTitle}</span>
|
||||
<button
|
||||
type="button"
|
||||
data-role="copy-address"
|
||||
title="Copy Address"
|
||||
className={styles.btnCopy}
|
||||
>
|
||||
<Copy
|
||||
role="img"
|
||||
aria-label="Copy Address"
|
||||
className={styles.copyIcon}
|
||||
/>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className={styles.toggleArrow}>
|
||||
<ChevronUp
|
||||
role="img"
|
||||
aria-label="Toggle"
|
||||
className={`${styles.icon} ${
|
||||
openConversations ? styles.isFlipped : ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
24
src/components/@shared/DirectMessages/List.module.css
Normal file
@ -0,0 +1,24 @@
|
||||
.conversations {
|
||||
height: 100%;
|
||||
background-color: var(--background-content);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.conversations .empty {
|
||||
height: 100%;
|
||||
max-width: 80%;
|
||||
text-align: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
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) / 2);
|
||||
}
|
45
src/components/@shared/DirectMessages/List.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { useOrbis } from '@context/DirectMessages'
|
||||
import { IConversationWithAdditionalData } from '@context/DirectMessages/_types'
|
||||
import ListItem from './ListItem'
|
||||
import ChatBubble from '@images/chatbubble.svg'
|
||||
import styles from './List.module.css'
|
||||
|
||||
export default function List() {
|
||||
const { conversations, setConversationId } = useOrbis()
|
||||
|
||||
const filteredConversations = useMemo(() => {
|
||||
return conversations.filter(
|
||||
(conversation: IConversationWithAdditionalData) =>
|
||||
!conversation.empty_message
|
||||
)
|
||||
}, [conversations])
|
||||
|
||||
return (
|
||||
<div className={styles.conversations}>
|
||||
{filteredConversations.length > 0 ? (
|
||||
filteredConversations.map(
|
||||
(conversation: IConversationWithAdditionalData, index: number) => (
|
||||
<ListItem
|
||||
key={index}
|
||||
conversation={conversation}
|
||||
setConversationId={setConversationId}
|
||||
/>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<div className={styles.empty}>
|
||||
<ChatBubble role="img" aria-label="Chat" className={styles.icon} />
|
||||
<p>
|
||||
Hello! Any question regarding a specific dataset listed on Ocean
|
||||
Marketplace?
|
||||
</p>
|
||||
<p>
|
||||
Go over the asset detail page or directly to a publisher profile to
|
||||
start a conversation!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
62
src/components/@shared/DirectMessages/ListItem.module.css
Normal file
@ -0,0 +1,62 @@
|
||||
.conversation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: calc(0.5 * var(--spacer)) calc(0.5 * var(--spacer));
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.conversation:hover {
|
||||
background: var(--background-highlight);
|
||||
}
|
||||
|
||||
.accountAvatarSet {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.accountAvatar {
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 100%;
|
||||
border: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
width: calc(var(--font-size-large) * 2.5);
|
||||
height: calc(var(--font-size-large) * 2.5);
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.notificationCount {
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
background-color: var(--color-primary);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--brand-white);
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-mini);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.accountInfo {
|
||||
padding-left: calc(var(--spacer) / 2);
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.accountName {
|
||||
font-size: var(--font-size-medium);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.lastMessageDate {
|
||||
font-size: var(--font-size-mini);
|
||||
color: var(--font-color-text);
|
||||
min-width: calc(var(--spacer) * 1.5);
|
||||
text-align: right;
|
||||
}
|
66
src/components/@shared/DirectMessages/ListItem.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
import { useOrbis } from '@context/DirectMessages'
|
||||
import { IConversationWithAdditionalData } from '@context/DirectMessages/_types'
|
||||
import { didToAddress } from './_utils'
|
||||
import Avatar from '@shared/atoms/Avatar'
|
||||
import Time from '@shared/atoms/Time'
|
||||
import styles from './ListItem.module.css'
|
||||
|
||||
export default function ConversationItem({
|
||||
conversation,
|
||||
setConversationId
|
||||
}: {
|
||||
conversation: IConversationWithAdditionalData
|
||||
setConversationId: (value: string) => void
|
||||
}) {
|
||||
const { account, getConversationTitle } = useOrbis()
|
||||
|
||||
const newCancelToken = useCancelToken()
|
||||
|
||||
const [name, setName] = useState<string>(null)
|
||||
const [address, setAddress] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
const getProfile = async () => {
|
||||
const did = conversation.recipients.find((o) => o !== account.did)
|
||||
|
||||
const _address = didToAddress(did)
|
||||
setAddress(_address)
|
||||
|
||||
const _name = await getConversationTitle(conversation?.stream_id)
|
||||
setName(_name)
|
||||
}
|
||||
|
||||
if (conversation && account) {
|
||||
getProfile()
|
||||
}
|
||||
}, [conversation, account, newCancelToken, getConversationTitle])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.conversation}
|
||||
onClick={() => setConversationId(conversation.stream_id)}
|
||||
>
|
||||
<div className={styles.accountAvatarSet}>
|
||||
<Avatar accountId={address} className={styles.accountAvatar} />
|
||||
{conversation.notifications_count > 0 && (
|
||||
<div className={styles.notificationCount}>
|
||||
{conversation.notifications_count}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.accountInfo}>
|
||||
<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>
|
||||
)
|
||||
}
|
89
src/components/@shared/DirectMessages/Postbox.module.css
Normal file
@ -0,0 +1,89 @@
|
||||
.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);
|
||||
}
|
||||
|
||||
.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;
|
||||
padding: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.sendButton {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
color: var(--color-primary);
|
||||
width: 41px;
|
||||
height: 41px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-left: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sendButton:disabled {
|
||||
color: var(--color-secondary);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.sendButton .icon {
|
||||
width: 24px;
|
||||
fill: currentColor;
|
||||
}
|
121
src/components/@shared/DirectMessages/Postbox.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import React, { useRef } from 'react'
|
||||
import styles from './Postbox.module.css'
|
||||
import { useOrbis } from '@context/DirectMessages'
|
||||
import SendIcon from '@images/send.svg'
|
||||
import { accountTruncate } from '@utils/web3'
|
||||
import { didToAddress } from './_utils'
|
||||
import {
|
||||
IOrbisMessage,
|
||||
IOrbisMessageContent
|
||||
} from '@context/DirectMessages/_types'
|
||||
|
||||
export default function Postbox({
|
||||
replyTo = null,
|
||||
cancelReplyTo,
|
||||
callback
|
||||
}: {
|
||||
replyTo?: IOrbisMessage
|
||||
cancelReplyTo?: () => void
|
||||
callback: (value: IOrbisMessage) => void
|
||||
}) {
|
||||
const postBoxArea = useRef(null)
|
||||
const {
|
||||
orbis,
|
||||
account,
|
||||
conversationId,
|
||||
updateConversationEmptyMessageStatus
|
||||
} = useOrbis()
|
||||
|
||||
const share = async () => {
|
||||
if (!account || postBoxArea.current.innerText.trim() === '') return false
|
||||
|
||||
const body = postBoxArea.current.innerText.trim()
|
||||
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 timestamp = Math.floor(Date.now() / 1000)
|
||||
|
||||
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,
|
||||
timestamp
|
||||
}
|
||||
|
||||
if (callback) callback(_callbackContent)
|
||||
|
||||
const res = await orbis.sendMessage({
|
||||
conversation_id: conversationId,
|
||||
body
|
||||
})
|
||||
|
||||
if (res.status === 200) {
|
||||
_callbackContent.stream_id = res.doc
|
||||
if (callback) callback(_callbackContent)
|
||||
updateConversationEmptyMessageStatus(conversationId, false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (!e.key) return
|
||||
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
// Don't generate a new line
|
||||
e.preventDefault()
|
||||
share()
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.sendButton}
|
||||
onClick={share}
|
||||
disabled={!conversationId}
|
||||
>
|
||||
<SendIcon className={styles.icon} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
97
src/components/@shared/DirectMessages/_utils.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import React, { ReactNode } from 'react'
|
||||
import Link from 'next/link'
|
||||
import reactStringReplace from 'react-string-replace'
|
||||
import { IOrbisPostContent } from '@context/DirectMessages/_types'
|
||||
|
||||
/** Regex patterns to use */
|
||||
const patternMentions = /\B@[a-z0-9_.⍙-]+/gi
|
||||
|
||||
export function didToAddress(did: string) {
|
||||
if (!did) return
|
||||
|
||||
const _did = did.split(':')
|
||||
return _did[4]
|
||||
}
|
||||
|
||||
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} />
|
||||
})
|
||||
|
||||
body = reactStringReplace(body, '\n', function (match, i) {
|
||||
return <br key={match + i} />
|
||||
})
|
||||
|
||||
/** Replace URLs */
|
||||
body = reactStringReplace(
|
||||
body,
|
||||
/(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/g,
|
||||
function (match, i) {
|
||||
const shortUrl =
|
||||
match.substring(0, 25) +
|
||||
'...' +
|
||||
match.substring(match.length - 15, match.length)
|
||||
return (
|
||||
<a
|
||||
key={match + i}
|
||||
href={match}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
title={match}
|
||||
>
|
||||
{match.length > 40 ? shortUrl : match}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
/** Identify and replace mentions */
|
||||
|
||||
/** Get mentions in post metadata */
|
||||
const { mentions } = content
|
||||
|
||||
/** Retrieve mentions in the body */
|
||||
const mentionsInBody = content.body.toString().match(patternMentions)
|
||||
|
||||
/** Compare both and replace in body */
|
||||
if (mentionsInBody && mentions && Array.isArray(mentions)) {
|
||||
mentionsInBody.forEach((_m) => {
|
||||
/** Find mention with the same name */
|
||||
const mention = mentions.find((obj) => obj.username === _m)
|
||||
if (mention !== undefined) {
|
||||
body = reactStringReplace(body, _m, (match, i) =>
|
||||
mention.did ? (
|
||||
<Link
|
||||
href={`/profile/${didToAddress(mention.did)}`}
|
||||
key={match + i}
|
||||
>
|
||||
{mention.username}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="link" key={i}>
|
||||
{mention.username}
|
||||
</span>
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
export const sleep = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms))
|
73
src/components/@shared/DirectMessages/index.module.css
Normal file
@ -0,0 +1,73 @@
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
padding: 0 calc(var(--spacer) / 3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transition: transform 300ms ease 0s;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.floating {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
pointer-events: auto;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: 530px;
|
||||
max-height: 80vh;
|
||||
background-color: var(--background-content);
|
||||
border: 1px solid var(--border-color);
|
||||
border-bottom: 0;
|
||||
border-radius: var(--border-radius);
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.headerWrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bodyWrapper {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.walletWrapper {
|
||||
margin: auto;
|
||||
max-width: 320px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.walletWrapper p {
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.walletWrapper button {
|
||||
margin: var(--spacer) auto 0;
|
||||
}
|
||||
|
||||
.isClosed {
|
||||
transform: translateY(90%);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 42rem) {
|
||||
.wrapper {
|
||||
padding: 0 calc(var(--spacer) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 55rem) {
|
||||
.wrapper {
|
||||
padding: 0 var(--spacer);
|
||||
}
|
||||
}
|
107
src/components/@shared/DirectMessages/index.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import React from 'react'
|
||||
import styles from './index.module.css'
|
||||
import Conversation from './Conversation'
|
||||
import { useOrbis } from '@context/DirectMessages'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
import Header from './Header'
|
||||
import List from './List'
|
||||
import walletStyles from '../../Header/Wallet/Account.module.css'
|
||||
|
||||
const BodyContent = () => {
|
||||
const { account, conversationId, checkOrbisConnection } = useOrbis()
|
||||
const { accountId, connect } = useWeb3()
|
||||
|
||||
const handleActivation = async (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
const resConnect = await connect()
|
||||
if (resConnect) {
|
||||
await checkOrbisConnection({
|
||||
address: accountId,
|
||||
autoConnect: true,
|
||||
lit: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const message = () => {
|
||||
return (
|
||||
<>
|
||||
<p>A new decentralized, encrypted private messaging is here!</p>
|
||||
<p>
|
||||
Engage with data publishers, get your algorithms whitelisted and
|
||||
establish trust.
|
||||
</p>
|
||||
<p>
|
||||
You'll be required to sign 2 transactions, one to connect to your
|
||||
decentralized identity and the other to generate your encrypted key.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (!accountId) {
|
||||
return (
|
||||
<div className={styles.walletWrapper}>
|
||||
<div>
|
||||
<h5>Connect your wallet to start messaging</h5>
|
||||
{message()}
|
||||
<button
|
||||
className={`${walletStyles.button} ${walletStyles.initial}`}
|
||||
onClick={(e) => handleActivation(e)}
|
||||
>
|
||||
Connect <span>Wallet</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
return (
|
||||
<div className={styles.walletWrapper}>
|
||||
<div>
|
||||
<h5>Sign your wallet to start messaging</h5>
|
||||
{message()}
|
||||
<button
|
||||
className={`${walletStyles.button} ${walletStyles.initial}`}
|
||||
onClick={() =>
|
||||
checkOrbisConnection({
|
||||
address: accountId,
|
||||
autoConnect: true,
|
||||
lit: true
|
||||
})
|
||||
}
|
||||
>
|
||||
Sign <span>Wallet</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<List />
|
||||
{conversationId && <Conversation />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function DirectMessages() {
|
||||
const { openConversations } = useOrbis()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.wrapper} ${!openConversations && styles.isClosed}`}
|
||||
>
|
||||
<div className={styles.floating}>
|
||||
<div className={styles.headerWrapper}>
|
||||
<Header />
|
||||
</div>
|
||||
<div className={styles.bodyWrapper}>
|
||||
<BodyContent />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -2,5 +2,4 @@
|
||||
composes: box from '@shared/atoms/Box.module.css';
|
||||
max-width: 35rem;
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React, { ReactElement, useState, useEffect } from 'react'
|
||||
import Compute from './Compute'
|
||||
import Consume from './Download'
|
||||
import Download from './Download'
|
||||
import { FileInfo, LoggerInstance, Datatoken } from '@oceanprotocol/lib'
|
||||
import Tabs, { TabsItem } from '@shared/atoms/Tabs'
|
||||
import { compareAsBN } from '@utils/numbers'
|
||||
import { useAsset } from '@context/Asset'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
@ -144,8 +143,8 @@ export default function AssetActions({
|
||||
}
|
||||
}, [balance, accountId, asset?.accessDetails, dtBalance])
|
||||
|
||||
const UseContent = (
|
||||
<>
|
||||
return (
|
||||
<div className={styles.actions}>
|
||||
{isCompute ? (
|
||||
<Compute
|
||||
asset={asset}
|
||||
@ -154,7 +153,7 @@ export default function AssetActions({
|
||||
fileIsLoading={fileIsLoading}
|
||||
/>
|
||||
) : (
|
||||
<Consume
|
||||
<Download
|
||||
asset={asset}
|
||||
dtBalance={dtBalance}
|
||||
isBalanceSufficient={isBalanceSufficient}
|
||||
@ -163,19 +162,6 @@ export default function AssetActions({
|
||||
/>
|
||||
)}
|
||||
<AssetStats />
|
||||
</>
|
||||
)
|
||||
|
||||
const tabs: TabsItem[] = [{ title: 'Use', content: UseContent }]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs items={tabs} className={styles.actions} />
|
||||
<Web3Feedback
|
||||
networkId={asset?.chainId}
|
||||
accountId={accountId}
|
||||
isAssetNetwork={isAssetNetwork}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -16,6 +16,9 @@ import content from '../../../../content/purgatory.json'
|
||||
import Web3 from 'web3'
|
||||
import Button from '@shared/atoms/Button'
|
||||
import RelatedAssets from '../RelatedAssets'
|
||||
import DmButton from '@shared/DirectMessages/DmButton'
|
||||
import Web3Feedback from '@components/@shared/Web3Feedback'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
|
||||
export default function AssetContent({
|
||||
asset
|
||||
@ -23,6 +26,7 @@ export default function AssetContent({
|
||||
asset: AssetExtended
|
||||
}): ReactElement {
|
||||
const { isInPurgatory, purgatoryData, isOwner, isAssetNetwork } = useAsset()
|
||||
const { accountId } = useWeb3()
|
||||
const { debug } = useUserPreferences()
|
||||
const [receipts, setReceipts] = useState([])
|
||||
const [nftPublisher, setNftPublisher] = useState<string>()
|
||||
@ -79,6 +83,14 @@ export default function AssetContent({
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.ownerActions}>
|
||||
<DmButton accountId={asset?.nft?.owner} />
|
||||
</div>
|
||||
<Web3Feedback
|
||||
networkId={asset?.chainId}
|
||||
accountId={accountId}
|
||||
isAssetNetwork={isAssetNetwork}
|
||||
/>
|
||||
<RelatedAssets />
|
||||
</div>
|
||||
</article>
|
||||
|
@ -1,5 +1,5 @@
|
||||
.footer {
|
||||
padding: var(--spacer) calc(var(--spacer) / 2);
|
||||
padding: var(--spacer) calc(var(--spacer) / 2) calc(var(--spacer) * 2.5);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: var(--layout-max-width);
|
||||
|
@ -5,11 +5,13 @@ import Button from '@shared/atoms/Button'
|
||||
import AddToken from '@shared/AddToken'
|
||||
import Conversion from '@shared/Price/Conversion'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
import { useOrbis } from '@context/DirectMessages'
|
||||
import { getOceanConfig } from '@utils/ocean'
|
||||
import styles from './Details.module.css'
|
||||
|
||||
export default function Details(): ReactElement {
|
||||
const {
|
||||
accountId,
|
||||
web3ProviderInfo,
|
||||
web3Modal,
|
||||
connect,
|
||||
@ -18,6 +20,7 @@ export default function Details(): ReactElement {
|
||||
networkId,
|
||||
balance
|
||||
} = useWeb3()
|
||||
const { checkOrbisConnection, disconnectOrbis } = useOrbis()
|
||||
const { locale } = useUserPreferences()
|
||||
|
||||
const [mainCurrency, setMainCurrency] = useState<string>()
|
||||
@ -83,6 +86,7 @@ export default function Details(): ReactElement {
|
||||
onClick={async () => {
|
||||
await web3Modal?.clearCachedProvider()
|
||||
connect()
|
||||
checkOrbisConnection({ address: accountId })
|
||||
}}
|
||||
>
|
||||
Switch Wallet
|
||||
@ -92,6 +96,7 @@ export default function Details(): ReactElement {
|
||||
size="small"
|
||||
onClick={() => {
|
||||
logout()
|
||||
disconnectOrbis(accountId)
|
||||
location.reload()
|
||||
}}
|
||||
>
|
||||
|
@ -2,6 +2,13 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sendMessage {
|
||||
display: flex;
|
||||
position: justify-end;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.account p {
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ export default function Account({
|
||||
<Jellyfish className={styles.image} />
|
||||
)}
|
||||
</figure>
|
||||
|
||||
<div>
|
||||
<h3 className={styles.name}>
|
||||
{profile?.name || accountTruncate(accountId)}
|
||||
|
@ -20,6 +20,11 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.directMessage {
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 50rem) {
|
||||
.grid {
|
||||
display: grid;
|
||||
@ -32,6 +37,11 @@
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
-webkit-line-clamp: 7 !important;
|
||||
}
|
||||
|
||||
.directMessage {
|
||||
margin-top: 0;
|
||||
text-align: end;
|
||||
}
|
||||
}
|
||||
|
||||
.publisherLinks {
|
||||
|
@ -5,6 +5,7 @@ import Stats from './Stats'
|
||||
import Account from './Account'
|
||||
import styles from './index.module.css'
|
||||
import { useProfile } from '@context/Profile'
|
||||
import DmButton from '@shared/DirectMessages/DmButton'
|
||||
|
||||
const isDescriptionTextClamped = () => {
|
||||
const el = document.getElementById('description')
|
||||
@ -39,6 +40,9 @@ export default function AccountHeader({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={styles.directMessage}>
|
||||
<DmButton accountId={accountId} />
|
||||
</div>
|
||||
<Markdown text={profile?.description} className={styles.description} />
|
||||
{isDescriptionTextClamped() ? (
|
||||
<span className={styles.more} onClick={toogleShowMore}>
|
||||
|
@ -6,7 +6,8 @@ import { UserPreferencesProvider } from '@context/UserPreferences'
|
||||
import PricesProvider from '@context/Prices'
|
||||
import UrqlProvider from '@context/UrqlProvider'
|
||||
import ConsentProvider from '@context/CookieConsent'
|
||||
import App from '../../src/components/App'
|
||||
import { OrbisProvider } from '@context/DirectMessages'
|
||||
import App from 'src/components/App'
|
||||
|
||||
import '@oceanprotocol/typographies/css/ocean-typo.css'
|
||||
import '../stylesGlobal/styles.css'
|
||||
@ -45,11 +46,13 @@ function MyApp({ Component, pageProps }: AppProps): ReactElement {
|
||||
<UserPreferencesProvider>
|
||||
<PricesProvider>
|
||||
<ConsentProvider>
|
||||
<OrbisProvider>
|
||||
<PostHogProvider client={posthog}>
|
||||
<App>
|
||||
<Component {...pageProps} />
|
||||
</App>
|
||||
</PostHogProvider>
|
||||
</OrbisProvider>
|
||||
</ConsentProvider>
|
||||
</PricesProvider>
|
||||
</UserPreferencesProvider>
|
||||
|
48
src/stylesGlobal/_emojipicker.css
Normal file
@ -0,0 +1,48 @@
|
||||
.EmojiPickerReact * {
|
||||
font-family: var(--font-family-base) !important;
|
||||
}
|
||||
.EmojiPickerReact.epr-dark-theme {
|
||||
--epr-bg-color: var(--background-content) !important;
|
||||
--epr-category-label-bg-color: #141414e6 !important;
|
||||
}
|
||||
.EmojiPickerReact {
|
||||
--epr-emoji-size: 24px !important;
|
||||
}
|
||||
|
||||
.EmojiPickerReact .epr-search-container input.epr-search {
|
||||
background-color: transparent !important;
|
||||
border-color: var(--border-color) !important;
|
||||
border-radius: var(--border-radius) !important;
|
||||
color: var(--brand-black) !important;
|
||||
}
|
||||
|
||||
.EmojiPickerReact .epr-search-container input.epr-search:focus {
|
||||
background-color: var(--epr-search-input-bg-color-active);
|
||||
border-color: var(--brand-black) !important;
|
||||
}
|
||||
|
||||
.EmojiPickerReact.epr-dark-theme .epr-search-container input.epr-search {
|
||||
color: var(--brand-white) !important;
|
||||
}
|
||||
|
||||
.EmojiPickerReact.epr-dark-theme .epr-search-container input.epr-search:focus {
|
||||
border-color: var(--brand-white) !important;
|
||||
}
|
||||
|
||||
.EmojiPickerReact .epr-category-nav > button.epr-cat-btn {
|
||||
-webkit-filter: hue-rotate(110deg) saturate(2);
|
||||
filter: hue-rotate(110deg) saturate(2);
|
||||
}
|
||||
|
||||
.EmojiPickerReact .epr-body::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.EmojiPickerReact .epr-body {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
aside.EmojiPickerReact.epr-main {
|
||||
border-width: 0 !important;
|
||||
}
|
@ -163,6 +163,7 @@ table th {
|
||||
@import '_code.css';
|
||||
@import '_toast.css';
|
||||
@import '_web3modal.css';
|
||||
@import '_emojipicker.css';
|
||||
|
||||
/* prevent background scrolling */
|
||||
.ReactModal__Body--open {
|
||||
|