diff --git a/lib/src/constants.ts b/lib/src/constants.ts index 481d58c..6ce7308 100644 --- a/lib/src/constants.ts +++ b/lib/src/constants.ts @@ -2,3 +2,7 @@ export const MESSAGE_THREAD_ID_PREFIX = "PUBNUB_INTERNAL_THREAD" export const INTERNAL_MODERATION_PREFIX = "PUBNUB_INTERNAL_MODERATION_" export const INTERNAL_ADMIN_CHANNEL = "PUBNUB_INTERNAL_ADMIN_CHANNEL" export const ERROR_LOGGER_KEY_PREFIX = "PUBNUB_INTERNAL_ERROR_LOGGER" +export const INTERNAL_MODERATOR_DATA = { + id: "PUBNUB_INTERNAL_MODERATOR", + type: "internal", +} diff --git a/lib/src/entities/message.ts b/lib/src/entities/message.ts index e437c19..3c22c6f 100644 --- a/lib/src/entities/message.ts +++ b/lib/src/entities/message.ts @@ -202,6 +202,10 @@ export class Message { async editText(newText: string) { const type = this.chat.editMessageActionName try { + if (this.meta?.PUBNUB_INTERNAL_AUTOMODERATED && !this.chat.currentUser.isInternalModerator) { + throw "The automoderated message can no longer be edited" + } + const { data } = await this.chat.sdk.addMessageAction({ channel: this.channelId, messageTimetoken: this.timetoken, diff --git a/lib/src/entities/user.ts b/lib/src/entities/user.ts index f00baa0..b2ffae2 100644 --- a/lib/src/entities/user.ts +++ b/lib/src/entities/user.ts @@ -3,7 +3,11 @@ import { Chat } from "./chat" import { DeleteParameters, OptionalAllBut } from "../types" import { Channel } from "./channel" import { Membership } from "./membership" -import { INTERNAL_ADMIN_CHANNEL, INTERNAL_MODERATION_PREFIX } from "../constants" +import { + INTERNAL_ADMIN_CHANNEL, + INTERNAL_MODERATION_PREFIX, + INTERNAL_MODERATOR_DATA, +} from "../constants" import { getErrorProxiedEntity } from "../error-logging" export type UserFields = Pick< @@ -56,6 +60,11 @@ export class User { return getErrorProxiedEntity(new User(chat, data), chat.errorLogger) } + /** @internal */ + get isInternalModerator() { + return this.id === INTERNAL_MODERATOR_DATA.id && this.type === INTERNAL_MODERATOR_DATA.type + } + get active() { return !!( this.lastActiveTimestamp && diff --git a/samples/react-native-group-chat/App.tsx b/samples/react-native-group-chat/App.tsx index 4761228..3846e06 100644 --- a/samples/react-native-group-chat/App.tsx +++ b/samples/react-native-group-chat/App.tsx @@ -57,8 +57,9 @@ function MainRoutesNavigator({ route }: StackScreenProps void removeThreadReply?: boolean onPinMessage: (message: Message | ThreadMessage) => void + onEditMessage: (message: Message | ThreadMessage) => void onToggleEmoji: (message: Message) => void onDeleteMessage: (message: Message) => void } @@ -29,6 +30,7 @@ export function useActionsMenu({ onPinMessage, onToggleEmoji, onDeleteMessage, + onEditMessage, }: UseActionsMenuParams) { const bottomSheetModalRef = useRef(null) const navigation = useNavigation() @@ -38,7 +40,7 @@ export function useActionsMenu({ ) // variables - const snapPoints = useMemo(() => ["25%", "50%"], []) + const snapPoints = useMemo(() => ["25%", "50%", "75%"], []) // callbacks const handlePresentModalPress = useCallback(({ message }: { message: EnhancedIMessage }) => { @@ -63,7 +65,7 @@ export function useActionsMenu({ const ActionsMenuComponent = () => ( + {currentlyFocusedMessage?.originalPnMessage.userId === chat?.currentUser.id && ( + <> + + + + )} ) : null} {currentlyFocusedMessage?.originalPnMessage.userId === chat?.currentUser.id ? ( diff --git a/samples/react-native-group-chat/components/message-text/MessageText.tsx b/samples/react-native-group-chat/components/message-text/MessageText.tsx index 30d7142..1fb2cc8 100644 --- a/samples/react-native-group-chat/components/message-text/MessageText.tsx +++ b/samples/react-native-group-chat/components/message-text/MessageText.tsx @@ -10,10 +10,11 @@ import { useNavigation } from "@react-navigation/native" type MessageTextProps = { onGoToMessage: (message: Message) => void + isBeingEdited?: boolean messageProps: Bubble["props"] } -export function MessageText({ onGoToMessage, messageProps }: MessageTextProps) { +export function MessageText({ onGoToMessage, messageProps, isBeingEdited }: MessageTextProps) { const { chat, setCurrentChannel } = useContext(ChatContext) const navigation = useNavigation() const [imageSrc, setImageSrc] = useState("") @@ -146,7 +147,7 @@ export function MessageText({ onGoToMessage, messageProps }: MessageTextProps) { if (messageElements) { return ( - + {messageProps.currentMessage?.originalPnMessage.quotedMessage ? ( ) { } }, []) + const handleEditMessage = useCallback( + async (message: Message) => { + if (!currentChannel) { + return + } + const newMessageDraft = currentChannel.createMessageDraft({ + userSuggestionSource: "global", + isTypingIndicatorTriggered: currentChannel.type !== "public", + }) + newMessageDraft.value = message.text + setText(newMessageDraft.value) + setGiftedChatMappedMessages((curr) => + curr.map((m) => { + return { + ...m, + isBeingEdited: m.originalPnMessage.timetoken === message.timetoken, + } + }) + ) + + setMessageDraft(newMessageDraft) + }, + [currentChannel] + ) + const { ActionsMenuComponent, handlePresentModalPress } = useActionsMenu({ onQuote: handleQuote, onPinMessage: handlePin, onToggleEmoji: handleEmoji, onDeleteMessage: handleDeleteMessage, + onEditMessage: handleEditMessage, }) useEffect(() => { @@ -245,6 +271,17 @@ export function ChatScreen({}: StackScreenProps) { } }, [currentChannel, currentChannelMembership]) + function resetMessageBeingEdited() { + setGiftedChatMappedMessages((curr) => + curr.map((m) => { + return { + ...m, + isBeingEdited: false, + } + }) + ) + } + const resetInput = () => { if (!messageDraft) { return @@ -254,6 +291,11 @@ export function ChatScreen({}: StackScreenProps) { messageDraft.files = undefined setText("") setImage("") + resetMessageBeingEdited() + } + + function getMessageBeingEdited() { + return giftedChatMappedMessages.find((m) => m.isBeingEdited)?.originalPnMessage } const onSend = async () => { @@ -262,17 +304,29 @@ export function ChatScreen({}: StackScreenProps) { } try { - await messageDraft.send() + const messageBeingEdited = getMessageBeingEdited() + if (messageBeingEdited) { + await messageBeingEdited.editText(messageDraft.value) + } else { + await messageDraft.send() + } } catch (error) { + let alertFn = (_: string) => null + if (Platform.OS === "web") { + alertFn = alert + } else { + alertFn = Alert.alert + } + + if (typeof error === "string") { + alertFn(error) + resetMessageBeingEdited() + } const e = error as { status: { errorData: { status: number } } } if (e?.status?.errorData?.status !== 403) { return } - if (Platform.OS === "web") { - alert(`You cannot send messages to this channel: ${currentChannel?.id}`) - } else { - Alert.alert("You cannot send messages to this channel:", currentChannel?.id) - } + alertFn(`You cannot send messages to this channel: ${currentChannel?.id}`) } resetInput() } @@ -311,13 +365,25 @@ export function ChatScreen({}: StackScreenProps) { } const renderBubble = (props: Bubble["props"]) => { + const isBeingEditedStyles = props.currentMessage.isBeingEdited + ? { + borderWidth: 1, + borderColor: colors.teal700, + } + : {} + return ( {props.currentMessage?.originalPnMessage.hasThread ? ( @@ -384,7 +450,7 @@ export function ChatScreen({}: StackScreenProps) { onSend(messages)} + onSend={onSend} onInputTextChanged={handleInputChange} renderMessageText={renderMessageText} renderFooter={renderFooter} diff --git a/samples/react-native-group-chat/webpack.config.js b/samples/react-native-group-chat/webpack.config.js index f306dee..63ae71e 100644 --- a/samples/react-native-group-chat/webpack.config.js +++ b/samples/react-native-group-chat/webpack.config.js @@ -65,6 +65,11 @@ module.exports = async function (env, argv) { } }) + config.resolve.fallback = { + ...config.resolve.fallback, + crypto: require.resolve("expo-crypto"), + } + // Finally return the new config for the CLI to use. return config } diff --git a/yarn.lock b/yarn.lock index 93d9348..eccca99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6494,7 +6494,7 @@ __metadata: languageName: node linkType: hard -"base64-js@npm:^1.0.2, base64-js@npm:^1.2.3, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": +"base64-js@npm:^1.0.2, base64-js@npm:^1.2.3, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf @@ -9334,6 +9334,17 @@ __metadata: languageName: node linkType: hard +"expo-crypto@npm:^14.0.1": + version: 14.0.1 + resolution: "expo-crypto@npm:14.0.1" + dependencies: + base64-js: "npm:^1.3.0" + peerDependencies: + expo: "*" + checksum: 10c0/3a240c83c4e4282d6c23267efdb25276fba73c9848c98eca019e8b48326941662eeb131babab04fec5913cae02a62c7e5b8f161d1657fa2e59048fa429253a5a + languageName: node + linkType: hard + "expo-file-system@npm:~17.0.1": version: 17.0.1 resolution: "expo-file-system@npm:17.0.1" @@ -15518,6 +15529,7 @@ __metadata: babel-plugin-transform-inline-environment-variables: "npm:^0.4.4" expo: "npm:^51.0.38" expo-clipboard: "npm:~6.0.3" + expo-crypto: "npm:^14.0.1" expo-font: "npm:~12.0.10" expo-status-bar: "npm:~1.12.1" nanoid: "npm:4.0.2"