@river-build/sdk
Version:
For more details, visit the following resources:
1,080 lines • 45.1 kB
JavaScript
import { bin_toHexString } from '@river-build/dlog';
import { MembershipOp, ChannelMessage_Post_AttachmentSchema, ChannelMessage_PostSchema, } from '@river-build/proto';
import { isDefined, logNever, checkNever } from '../../../check';
import { EventStatus, MessageType, Membership, RiverTimelineEvent, } from './timeline-types';
import { userIdFromAddress, streamIdFromBytes, streamIdAsString } from '../../../id';
import { isLocalEvent, isRemoteEvent, isCiphertext, } from '../../../types';
import { getSpaceReviewEventDataBin } from '@river-build/web3';
import { create } from '@bufbuild/protobuf';
export function toEvent(timelineEvent, userId) {
const eventId = timelineEvent.hashStr;
const senderId = timelineEvent.creatorUserId;
// TODO: get sender metadata from store
const sender = {
id: senderId,
};
const { content, error } = toTownsContent(timelineEvent);
const isSender = sender.id === userId;
const fbc = `${content?.kind ?? '??'} ${getFallbackContent(sender.id, content, error)}`;
function extractSessionId(event) {
const payload = event.remoteEvent?.event.payload;
if (!payload ||
payload.case !== 'channelPayload' ||
payload.value.content.case !== 'message') {
return undefined;
}
return payload.value.content.value.sessionId;
}
const sessionId = extractSessionId(timelineEvent);
return {
eventId: eventId,
localEventId: timelineEvent.localEvent?.localId,
eventNum: timelineEvent.eventNum,
latestEventId: eventId,
latestEventNum: timelineEvent.eventNum,
status: isSender ? getEventStatus(timelineEvent) : EventStatus.RECEIVED,
createdAtEpochMs: Number(timelineEvent.createdAtEpochMs),
updatedAtEpochMs: undefined,
content: content,
fallbackContent: fbc,
isEncrypting: eventId.startsWith('~'),
isLocalPending: timelineEvent.remoteEvent === undefined,
isSendFailed: timelineEvent.localEvent?.status === 'failed',
confirmedEventNum: timelineEvent.confirmedEventNum,
confirmedInBlockNum: timelineEvent.miniblockNum,
threadParentId: getThreadParentId(content),
replyParentId: getReplyParentId(content),
reactionParentId: getReactionParentId(content),
isMentioned: getIsMentioned(content, userId),
isRedacted: false, // redacted is handled in use timeline store when the redaction event is received
sender,
sessionId,
};
}
function toTownsContent(timelineEvent) {
if (isLocalEvent(timelineEvent)) {
return toTownsContent_FromChannelMessage(timelineEvent.localEvent.channelMessage, 'local event', timelineEvent.hashStr);
}
else if (isRemoteEvent(timelineEvent)) {
return toTownsContent_fromParsedEvent(timelineEvent.hashStr, timelineEvent);
}
else {
return { error: 'unknown event content type' };
}
}
function validateEvent(eventId, message) {
let error;
if (!message.event.payload || !message.event.payload.value) {
error = { error: 'payloadless payload' };
return error;
}
if (!message.event.payload.case) {
error = { error: 'caseless payload' };
return error;
}
if (!message.event.payload.value.content.case) {
error = { error: `${message.event.payload.case} - caseless payload content` };
return error;
}
const description = `${message.event.payload.case}::${message.event.payload.value.content.case} id: ${eventId}`;
return { description };
}
function toTownsContent_fromParsedEvent(eventId, timelineEvent) {
const message = timelineEvent.remoteEvent;
const { error, description } = validateEvent(eventId, message);
if (error) {
return { error };
}
if (!description) {
return { error: 'no description' };
}
switch (message.event.payload.case) {
case 'userPayload':
return toTownsContent_UserPayload(timelineEvent, message, message.event.payload.value, description);
case 'channelPayload':
return toTownsContent_ChannelPayload(eventId, timelineEvent, message.event.payload.value, description);
case 'dmChannelPayload':
return toTownsContent_ChannelPayload(eventId, timelineEvent, message.event.payload.value, description);
case 'gdmChannelPayload':
return toTownsContent_ChannelPayload(eventId, timelineEvent, message.event.payload.value, description);
case 'spacePayload':
return toTownsContent_SpacePayload(eventId, message, message.event.payload.value, description);
case 'userMetadataPayload':
return {
error: `${description} userMetadataPayload not supported?`,
};
case 'userSettingsPayload':
return {
error: `${description} userSettingsPayload not supported?`,
};
case 'userInboxPayload':
return {
error: `${description} userInboxPayload not supported?`,
};
case 'miniblockHeader':
return toTownsContent_MiniblockHeader(eventId, message, message.event.payload.value, description);
case 'mediaPayload':
return {
error: `${description} mediaPayload not supported?`,
};
case 'memberPayload':
return toTownsContent_MemberPayload(timelineEvent, message, message.event.payload.value, description);
case undefined:
return { error: `Undefined payload case: ${description}` };
default:
logNever(message.event.payload);
return { error: `Unknown payload case: ${description}` };
}
}
function toTownsContent_MiniblockHeader(eventId, message, value, _description) {
return {
content: {
kind: RiverTimelineEvent.MiniblockHeader,
miniblockNum: value.miniblockNum,
hasSnapshot: value.snapshot !== undefined,
},
};
}
function toTownsContent_MemberPayload(event, message, value, description) {
switch (value.content.case) {
case 'membership':
return {
content: {
kind: RiverTimelineEvent.StreamMembership,
userId: userIdFromAddress(value.content.value.userAddress),
initiatorId: userIdFromAddress(value.content.value.initiatorAddress),
membership: toMembership(value.content.value.op),
},
};
case 'keySolicitation':
return {
content: {
kind: RiverTimelineEvent.KeySolicitation,
sessionIds: value.content.value.sessionIds,
deviceKey: value.content.value.deviceKey,
isNewDevice: value.content.value.isNewDevice,
},
};
case 'keyFulfillment':
return {
content: {
kind: RiverTimelineEvent.Fulfillment,
sessionIds: value.content.value.sessionIds,
deviceKey: value.content.value.deviceKey,
to: userIdFromAddress(value.content.value.userAddress),
from: message.creatorUserId,
},
};
case 'displayName':
return {
content: {
kind: RiverTimelineEvent.SpaceDisplayName,
userId: message.creatorUserId,
displayName: event.decryptedContent?.kind === 'text'
? event.decryptedContent.content
: value.content.value.ciphertext,
},
};
case 'username':
return {
content: {
kind: RiverTimelineEvent.SpaceUsername,
userId: message.creatorUserId,
username: event.decryptedContent?.kind === 'text'
? event.decryptedContent.content
: value.content.value.ciphertext,
},
};
case 'ensAddress':
return {
content: {
kind: RiverTimelineEvent.SpaceEnsAddress,
userId: message.creatorUserId,
ensAddress: value.content.value,
},
};
case 'nft':
return {
content: {
kind: RiverTimelineEvent.SpaceNft,
userId: message.creatorUserId,
contractAddress: bin_toHexString(value.content.value.contractAddress),
tokenId: bin_toHexString(value.content.value.tokenId),
chainId: value.content.value.chainId,
},
};
case 'pin':
return {
content: {
kind: RiverTimelineEvent.Pin,
userId: message.creatorUserId,
pinnedEventId: bin_toHexString(value.content.value.eventId),
},
};
case 'unpin':
return {
content: {
kind: RiverTimelineEvent.Unpin,
userId: message.creatorUserId,
unpinnedEventId: bin_toHexString(value.content.value.eventId),
},
};
case 'encryptionAlgorithm':
return {
content: {
kind: RiverTimelineEvent.StreamEncryptionAlgorithm,
algorithm: value.content.value.algorithm,
},
};
case 'memberBlockchainTransaction': {
const fromUserAddress = value.content.value.fromUserAddress;
const transaction = value.content.value.transaction;
if (!transaction) {
return { error: `${description} no transaction` };
}
if (!transaction.receipt?.transactionHash) {
return { error: `${description} no transactionHash` };
}
switch (transaction.content.case) {
case 'tip': {
const tipContent = transaction.content.value;
if (!tipContent.event) {
return { error: `${description} no event in tip` };
}
return {
content: {
kind: RiverTimelineEvent.TipEvent,
transaction: transaction,
tip: tipContent,
transactionHash: bin_toHexString(transaction.receipt.transactionHash),
fromUserId: userIdFromAddress(fromUserAddress),
refEventId: bin_toHexString(tipContent.event.messageId),
toUserId: userIdFromAddress(tipContent.toUserAddress),
},
};
}
case 'tokenTransfer':
return {
content: {
kind: RiverTimelineEvent.TokenTransfer,
transaction: transaction,
transfer: transaction.content.value,
fromUserId: userIdFromAddress(fromUserAddress),
createdAtEpochMs: event.createdAtEpochMs,
threadParentId: bin_toHexString(transaction.content.value.messageId),
},
};
case 'spaceReview': {
if (!transaction.receipt) {
return { error: `${description} no receipt` };
}
const reviewContent = transaction.content.value;
if (!reviewContent.event) {
return { error: `${description} no event in space review` };
}
const { comment, rating } = getSpaceReviewEventDataBin(transaction.receipt.logs, reviewContent.event.user);
return {
content: {
kind: RiverTimelineEvent.SpaceReview,
action: reviewContent.action,
rating: rating,
comment: comment,
fromUserId: userIdFromAddress(fromUserAddress),
},
};
}
case undefined:
return {
content: {
kind: RiverTimelineEvent.MemberBlockchainTransaction,
transaction: value.content.value.transaction,
fromUserId: bin_toHexString(value.content.value.fromUserAddress),
},
};
default:
logNever(transaction.content);
return { error: `${description} unknown transaction content` };
}
}
case undefined:
return { error: `Undefined payload case: ${description}` };
default:
logNever(value.content);
return {
error: `Unknown payload case: ${description}`,
};
}
}
function toTownsContent_UserPayload(event, message, value, description) {
switch (value.content.case) {
case 'inception': {
return {
content: {
kind: RiverTimelineEvent.Inception,
creatorId: message.creatorUserId,
type: message.event.payload.case,
},
};
}
case 'userMembership': {
const payload = value.content.value;
const streamId = streamIdFromBytes(payload.streamId);
return {
content: {
kind: RiverTimelineEvent.StreamMembership,
userId: '', // this is just the current user
initiatorId: '',
membership: toMembership(payload.op),
streamId: streamId,
},
};
}
case 'userMembershipAction': {
// these are admin actions where you can invite, join, or kick someone
const payload = value.content.value;
return {
content: {
kind: RiverTimelineEvent.StreamMembership,
userId: userIdFromAddress(payload.userId),
initiatorId: event.remoteEvent.creatorUserId,
membership: toMembership(payload.op),
},
};
}
case 'blockchainTransaction': {
const payload = value.content.value;
return {
content: {
kind: RiverTimelineEvent.UserBlockchainTransaction,
transaction: payload,
},
};
}
case 'receivedBlockchainTransaction': {
const payload = value.content.value;
return {
content: {
kind: RiverTimelineEvent.UserReceivedBlockchainTransaction,
receivedTransaction: payload,
},
};
}
case undefined: {
return { error: `Undefined payload case: ${description}` };
}
default: {
logNever(value.content);
return {
error: `Unknown payload case: ${description}`,
};
}
}
}
function toTownsContent_ChannelPayload(eventId, timelineEvent, value, description) {
const message = timelineEvent.remoteEvent;
switch (value.content.case) {
case 'inception': {
return {
content: {
kind: RiverTimelineEvent.Inception,
creatorId: message.creatorUserId,
type: message.event.payload.case,
},
};
}
case 'message': {
if (timelineEvent.decryptedContent?.kind === 'channelMessage') {
return toTownsContent_FromChannelMessage(timelineEvent.decryptedContent.content, description, eventId);
}
const payload = value.content.value;
return toTownsContent_ChannelPayload_Message(timelineEvent, payload, description);
}
case 'channelProperties': {
const payload = value.content.value;
return toTownsContent_ChannelPayload_ChannelProperties(timelineEvent, payload, description);
}
case 'redaction':
return {
content: {
kind: RiverTimelineEvent.RedactionActionEvent,
refEventId: bin_toHexString(value.content.value.eventId),
adminRedaction: true,
},
};
case undefined: {
return { error: `Undefined payload case: ${description}` };
}
default:
logNever(value.content);
return { error: `Unknown payload case: ${description}` };
}
}
function toTownsContent_FromChannelMessage(channelMessage, description, eventId) {
switch (channelMessage.payload?.case) {
case 'post':
return (toTownsContent_ChannelPayload_Message_Post(channelMessage.payload.value, eventId, undefined, description) ?? {
error: `${description} unknown message type`,
});
case 'reaction':
return {
content: {
kind: RiverTimelineEvent.Reaction,
reaction: channelMessage.payload.value.reaction,
targetEventId: channelMessage.payload.value.refEventId,
},
};
case 'redaction':
return {
content: {
kind: RiverTimelineEvent.RedactionActionEvent,
refEventId: channelMessage.payload.value.refEventId,
adminRedaction: false,
},
};
case 'edit': {
const newPost = channelMessage.payload.value.post;
if (!newPost) {
return { error: `${description} no post in edit` };
}
const newContent = toTownsContent_ChannelPayload_Message_Post(newPost, eventId, channelMessage.payload.value.refEventId, description);
return newContent ?? { error: `${description} no content in edit` };
}
case undefined:
return { error: `Undefined payload case: ${description}` };
default: {
logNever(channelMessage.payload);
return {
error: `Unknown payload case: ${description}`,
};
}
}
}
function toTownsContent_ChannelPayload_Message(timelineEvent, payload, description) {
if (isCiphertext(payload.ciphertext)) {
if (payload.refEventId) {
return {
content: {
kind: RiverTimelineEvent.ChannelMessageEncryptedWithRef,
refEventId: payload.refEventId,
},
};
}
return {
// if payload is an EncryptedData message, than it is encrypted content kind
content: {
kind: RiverTimelineEvent.ChannelMessageEncrypted,
error: timelineEvent.decryptedContentError,
},
};
}
// do not handle non-encrypted messages that should be encrypted
return { error: `${description} message text invalid channel message` };
}
function toTownsContent_ChannelPayload_ChannelProperties(timelineEvent, payload, description) {
if (timelineEvent.decryptedContent?.kind === 'channelProperties') {
return {
content: {
kind: RiverTimelineEvent.ChannelProperties,
properties: timelineEvent.decryptedContent.content,
},
};
}
// If the payload is encrypted, we display nothing.
return { error: `${description} encrypted channel properties` };
}
function toTownsContent_ChannelPayload_Message_Post(value, eventId, editsEventId, description) {
switch (value.content.case) {
case 'text':
return {
content: {
kind: RiverTimelineEvent.ChannelMessage,
body: value.content.value.body,
threadId: value.threadId,
threadPreview: value.threadPreview,
replyId: value.replyId,
replyPreview: value.replyPreview,
mentions: value.content.value.mentions,
editsEventId: editsEventId,
content: {
msgType: MessageType.Text,
},
attachments: toAttachments(value.content.value.attachments, eventId),
},
};
case 'image':
return {
content: {
kind: RiverTimelineEvent.ChannelMessage,
body: value.content.value.title,
threadId: value.threadId,
threadPreview: value.threadPreview,
mentions: [],
editsEventId: editsEventId,
content: {
msgType: MessageType.Image,
info: value.content.value.info,
thumbnail: value.content.value.thumbnail,
},
},
};
case 'gm':
return {
content: {
kind: RiverTimelineEvent.ChannelMessage,
body: value.content.value.typeUrl,
threadId: value.threadId,
threadPreview: value.threadPreview,
mentions: [],
editsEventId: editsEventId,
content: {
msgType: MessageType.GM,
data: value.content.value.value,
},
},
};
case undefined:
return { error: `Undefined payload case: ${description}` };
default:
logNever(value.content);
return { error: `Unknown payload case: ${description}` };
}
}
function toTownsContent_SpacePayload(eventId, message, value, description) {
switch (value.content.case) {
case 'inception': {
return {
content: {
kind: RiverTimelineEvent.Inception,
creatorId: message.creatorUserId,
type: message.event.payload.case,
},
};
}
case 'channel': {
const payload = value.content.value;
const channelId = streamIdAsString(payload.channelId);
return {
content: {
kind: RiverTimelineEvent.ChannelCreate,
creatorId: message.creatorUserId,
channelId,
channelOp: payload.op,
channelSettings: payload.settings,
},
};
}
case 'updateChannelAutojoin': {
const payload = value.content.value;
const channelId = streamIdAsString(payload.channelId);
return {
content: {
kind: RiverTimelineEvent.SpaceUpdateAutojoin,
autojoin: payload.autojoin,
channelId: channelId,
},
};
}
case 'updateChannelHideUserJoinLeaveEvents': {
const payload = value.content.value;
const channelId = streamIdAsString(payload.channelId);
return {
content: {
kind: RiverTimelineEvent.SpaceUpdateHideUserJoinLeaves,
hideUserJoinLeaves: payload.hideUserJoinLeaveEvents,
channelId: channelId,
},
};
}
case 'spaceImage': {
return {
content: {
kind: RiverTimelineEvent.SpaceImage,
},
};
}
case undefined:
return { error: `Undefined payload case: ${description}` };
default:
logNever(value.content);
return { error: `Unknown payload case: ${description}` };
}
}
function getEventStatus(timelineEvent) {
if (timelineEvent.remoteEvent) {
return EventStatus.SENT;
}
else if (timelineEvent.localEvent && timelineEvent.hashStr.startsWith('~')) {
return EventStatus.ENCRYPTING;
}
else if (timelineEvent.localEvent) {
switch (timelineEvent.localEvent.status) {
case 'failed':
return EventStatus.NOT_SENT;
case 'sending':
return EventStatus.SENDING;
case 'sent':
return EventStatus.SENT;
default:
logNever(timelineEvent.localEvent.status);
return EventStatus.NOT_SENT;
}
}
else {
return EventStatus.NOT_SENT;
}
}
function toAttachments(attachments, parentEventId) {
return attachments
.map((attachment, index) => toAttachment(attachment, parentEventId, index))
.filter(isDefined);
}
function toAttachment(attachment, parentEventId, index) {
const id = `${parentEventId}-${index}`;
switch (attachment.content.case) {
case 'chunkedMedia': {
const info = attachment.content.value.info;
if (!info) {
return undefined;
}
const thumbnailInfo = attachment.content.value.thumbnail?.info;
const thumbnailContent = attachment.content.value.thumbnail?.content;
const thumbnail = thumbnailInfo && thumbnailContent
? {
info: thumbnailInfo,
content: thumbnailContent,
}
: undefined;
const encryption = attachment.content.value.encryption.case === 'aesgcm'
? attachment.content.value.encryption.value
: undefined;
if (!encryption) {
return undefined;
}
return {
type: 'chunked_media',
info,
streamId: attachment.content.value.streamId,
encryption,
id,
thumbnail: thumbnail,
};
}
case 'embeddedMedia': {
const info = attachment.content.value.info;
if (!info) {
return undefined;
}
return {
type: 'embedded_media',
info,
content: attachment.content.value.content,
id,
};
}
case 'image': {
const info = attachment.content.value.info;
if (!info) {
return undefined;
}
return { type: 'image', info, id };
}
case 'embeddedMessage': {
const content = attachment.content.value;
if (!content?.post || !content?.info) {
return undefined;
}
const channelMessageEvent = toTownsContent_ChannelPayload_Message_Post(content.post, content.info.messageId, undefined, '').content;
return channelMessageEvent?.kind === RiverTimelineEvent.ChannelMessage
? ({
type: 'embedded_message',
...content,
info: content.info,
channelMessageEvent: channelMessageEvent,
id,
})
: undefined;
}
case 'unfurledUrl': {
const content = attachment.content.value;
return {
type: 'unfurled_link',
url: content.url,
title: content.title,
description: content.description,
image: content.image,
id,
};
}
default:
return undefined;
}
}
// function isDMMessageEventBlocked(
// event: TimelineEvent,
// kind: SnapshotCaseType,
// streamClient: Client,
// ) {
// if (kind !== 'dmChannelContent') {
// return false
// }
// if (!streamClient?.userSettingsStreamId) {
// return false
// }
// const stream = streamClient.stream(streamClient.userSettingsStreamId)
// check(isDefined(stream), 'stream must be defined')
// return stream.view.userSettingsContent.isUserBlockedAt(event.sender.id, event.eventNum)
// }
export function getFallbackContent(senderDisplayName, content, error) {
if (error) {
return error;
}
if (!content) {
throw new Error('Either content or error should be defined');
}
switch (content.kind) {
case RiverTimelineEvent.MiniblockHeader:
return `Miniblock miniblockNum:${content.miniblockNum}, hasSnapshot:${content.hasSnapshot.toString()}`;
case RiverTimelineEvent.Reaction:
return `${senderDisplayName} reacted with ${content.reaction} to ${content.targetEventId}`;
case RiverTimelineEvent.Inception:
return content.type ? `type: ${content.type}` : '';
case RiverTimelineEvent.ChannelMessageEncrypted:
return `Decrypting...`;
case RiverTimelineEvent.StreamMembership: {
return `[${content.membership}] userId: ${content.userId} initiatorId: ${content.initiatorId}`;
}
case RiverTimelineEvent.ChannelMessage:
return `${senderDisplayName}: ${content.body}`;
case RiverTimelineEvent.ChannelProperties:
return `properties: ${content.properties.name ?? ''} ${content.properties.topic ?? ''}`;
case RiverTimelineEvent.SpaceUsername:
return `username: ${content.username}`;
case RiverTimelineEvent.SpaceDisplayName:
return `username: ${content.displayName}`;
case RiverTimelineEvent.SpaceEnsAddress:
return `ensAddress: ${bin_toHexString(content.ensAddress)}`;
case RiverTimelineEvent.SpaceNft:
return `contractAddress: ${content.contractAddress}, tokenId: ${content.tokenId}, chainId: ${content.chainId}`;
case RiverTimelineEvent.RedactedEvent:
return `~Redacted~`;
case RiverTimelineEvent.RedactionActionEvent:
return `Redacts ${content.refEventId} adminRedaction: ${content.adminRedaction}`;
case RiverTimelineEvent.ChannelCreate:
if (content.channelSettings !== undefined) {
return `channelId: ${content.channelId} autojoin: ${content.channelSettings.autojoin} hideUserJoinLeaves: ${content.channelSettings.hideUserJoinLeaveEvents}`;
}
return `channelId: ${content.channelId}`;
case RiverTimelineEvent.SpaceUpdateAutojoin:
return `channelId: ${content.channelId} autojoin: ${content.autojoin}`;
case RiverTimelineEvent.SpaceUpdateHideUserJoinLeaves:
return `channelId: ${content.channelId} hideUserJoinLeaves: ${content.hideUserJoinLeaves}`;
case RiverTimelineEvent.SpaceImage:
return `SpaceImage`;
case RiverTimelineEvent.Fulfillment:
return `Fulfillment sessionIds: ${content.sessionIds.length ? content.sessionIds.join(',') : 'forNewDevice: true'}, from: ${content.from} to: ${content.deviceKey}`;
case RiverTimelineEvent.KeySolicitation:
if (content.isNewDevice) {
return `KeySolicitation deviceKey: ${content.deviceKey}, newDevice: true`;
}
return `KeySolicitation deviceKey: ${content.deviceKey} sessionIds: ${content.sessionIds.length}`;
case RiverTimelineEvent.ChannelMessageMissing:
return `eventId: ${content.eventId}`;
case RiverTimelineEvent.ChannelMessageEncryptedWithRef:
return `refEventId: ${content.refEventId}`;
case RiverTimelineEvent.Pin:
return `pinnedEventId: ${content.pinnedEventId} by: ${content.userId}`;
case RiverTimelineEvent.Unpin:
return `unpinnedEventId: ${content.unpinnedEventId} by: ${content.userId}`;
case RiverTimelineEvent.UserBlockchainTransaction:
return getFallbackContent_BlockchainTransaction(content.transaction);
case RiverTimelineEvent.MemberBlockchainTransaction:
return `memberTransaction from: ${content.fromUserId} ${getFallbackContent_BlockchainTransaction(content.transaction)}`;
case RiverTimelineEvent.TipEvent:
return `tip from: ${content.fromUserId} to: ${content.toUserId} refEventId: ${content.refEventId} amount: ${content.tip.event?.amount.toString() ?? '??'}`;
case RiverTimelineEvent.TokenTransfer:
return `tokenTransfer from: ${content.fromUserId} amount: ${content.transfer.amount}`;
case RiverTimelineEvent.SpaceReview:
return `spaceReview from: ${content.fromUserId} rating: ${content.rating} comment: ${content.comment}`;
case RiverTimelineEvent.UserReceivedBlockchainTransaction:
return `kind: ${content.receivedTransaction.transaction?.content?.case ?? '??'} fromUserAddress: ${content.receivedTransaction.fromUserAddress
? bin_toHexString(content.receivedTransaction.fromUserAddress)
: ''}`;
case RiverTimelineEvent.StreamEncryptionAlgorithm:
return `algorithm: ${content.algorithm}`;
default:
checkNever(content); // these are client side after parsing events, everything should be covered
}
}
function getFallbackContent_BlockchainTransaction(transaction) {
if (!transaction) {
return '??';
}
switch (transaction.content.case) {
case 'tip':
if (!transaction.content.value?.event) {
return '??';
}
return `kind: ${transaction.content.case} messageId: ${bin_toHexString(transaction.content.value.event.messageId)} receiver: ${bin_toHexString(transaction.content.value.event.receiver)} amount: ${transaction.content.value.event.amount.toString()}`;
default:
return `kind: ${transaction.content.case ?? 'unspecified'}`;
}
}
export function transformAttachments(attachments) {
if (!attachments) {
return [];
}
return attachments
.map((attachment) => {
switch (attachment.type) {
case 'chunked_media':
return create(ChannelMessage_Post_AttachmentSchema, {
content: {
case: 'chunkedMedia',
value: {
info: attachment.info,
streamId: attachment.streamId,
encryption: {
case: 'aesgcm',
value: attachment.encryption,
},
thumbnail: {
info: attachment.thumbnail?.info,
content: attachment.thumbnail?.content,
},
},
},
});
case 'embedded_media':
return create(ChannelMessage_Post_AttachmentSchema, {
content: {
case: 'embeddedMedia',
value: {
info: attachment.info,
content: attachment.content,
},
},
});
case 'image':
return create(ChannelMessage_Post_AttachmentSchema, {
content: {
case: 'image',
value: {
info: attachment.info,
},
},
});
case 'embedded_message': {
const { channelMessageEvent, ...content } = attachment;
if (!channelMessageEvent) {
return;
}
const post = create(ChannelMessage_PostSchema, {
threadId: channelMessageEvent.threadId,
threadPreview: channelMessageEvent.threadPreview,
content: {
case: 'text',
value: {
...channelMessageEvent,
attachments: transformAttachments(channelMessageEvent.attachments),
},
},
});
const value = create(ChannelMessage_Post_AttachmentSchema, {
content: {
case: 'embeddedMessage',
value: {
...content,
post,
},
},
});
return value;
}
case 'unfurled_link':
return create(ChannelMessage_Post_AttachmentSchema, {
content: {
case: 'unfurledUrl',
value: {
url: attachment.url,
title: attachment.title,
description: attachment.description,
image: attachment.image
? {
height: attachment.image.height,
width: attachment.image.width,
url: attachment.image.url,
}
: undefined,
},
},
});
case 'ticker':
return create(ChannelMessage_Post_AttachmentSchema, {
content: {
case: 'ticker',
value: {
chainId: attachment.chainId,
address: attachment.address,
},
},
});
default:
logNever(attachment);
return undefined;
}
})
.filter(isDefined);
}
// function getEditsId(content: TimelineEvent_OneOf | undefined): string | undefined {
// return content?.kind === RiverEvent.ChannelMessage ? content.editsEventId : undefined
// }
// function getRedactsId(content: TimelineEvent_OneOf | undefined): string | undefined {
// return content?.kind === RiverEvent.RedactionActionEvent ? content.refEventId : undefined
// }
function getThreadParentId(content) {
return content?.kind === RiverTimelineEvent.ChannelMessage ? content.threadId : undefined;
}
function getReplyParentId(content) {
return content?.kind === RiverTimelineEvent.ChannelMessage ? content.replyId : undefined;
}
function getReactionParentId(content) {
return content?.kind === RiverTimelineEvent.Reaction ? content.targetEventId : undefined;
}
function getIsMentioned(content, userId) {
//TODO: comparison below should be changed as soon as this HNT-1576 will be resolved
return content?.kind === RiverTimelineEvent.ChannelMessage
? content.mentions.findIndex((x) => (x.userId ?? '')
.toLowerCase()
.localeCompare(userId.toLowerCase(), undefined, { sensitivity: 'base' }) == 0) >= 0
: false;
}
export function toMembership(membershipOp) {
switch (membershipOp) {
case MembershipOp.SO_JOIN:
return Membership.Join;
case MembershipOp.SO_INVITE:
return Membership.Invite;
case MembershipOp.SO_LEAVE:
return Membership.Leave;
case MembershipOp.SO_UNSPECIFIED:
return Membership.None;
case undefined:
return Membership.None;
default:
checkNever(membershipOp);
}
}
export function toReplacedMessageEvent(prev, next) {
if (!canReplaceEvent(prev, next)) {
return prev;
}
else if (next.content?.kind === RiverTimelineEvent.ChannelMessage &&
prev.content?.kind === RiverTimelineEvent.ChannelMessage) {
// when we replace an event, we copy the content up to the root event
// so we keep the prev id, but use the next content
const isLocalId = prev.eventId.startsWith('~');
const eventId = !isLocalId ? prev.eventId : next.eventId;
return {
...next,
eventId: eventId,
eventNum: prev.eventNum,
latestEventId: next.eventId,
latestEventNum: next.eventNum,
confirmedEventNum: prev.confirmedEventNum ?? next.confirmedEventNum,
confirmedInBlockNum: prev.confirmedInBlockNum ?? next.confirmedInBlockNum,
createdAtEpochMs: prev.createdAtEpochMs,
updatedAtEpochMs: next.createdAtEpochMs,
content: {
...next.content,
threadId: prev.content.threadId,
},
threadParentId: prev.threadParentId,
reactionParentId: prev.reactionParentId,
sender: prev.sender,
};
}
else if (next.content?.kind === RiverTimelineEvent.RedactedEvent) {
// for redacted events, carry over previous pointers to content
// we don't want to lose thread info
return {
...next,
eventId: prev.eventId,
eventNum: prev.eventNum,
latestEventId: next.eventId,
latestEventNum: next.eventNum,
confirmedEventNum: prev.confirmedEventNum ?? next.confirmedEventNum,
confirmedInBlockNum: prev.confirmedInBlockNum ?? next.confirmedInBlockNum,
createdAtEpochMs: prev.createdAtEpochMs,
updatedAtEpochMs: next.createdAtEpochMs,
threadParentId: prev.threadParentId,
reactionParentId: prev.reactionParentId,
};
}
else if (prev.content?.kind === RiverTimelineEvent.RedactedEvent) {
// replacing a redacted event should maintain the redacted state
return {
...prev,
latestEventId: next.eventId,
latestEventNum: next.eventNum,
confirmedEventNum: prev.confirmedEventNum ?? next.confirmedEventNum,
confirmedInBlockNum: prev.confirmedInBlockNum ?? next.confirmedInBlockNum,
};
}
else {
// make sure we carry the createdAtEpochMs of the previous event
// so we don't end up with a timeline that has events out of order.
return {
...next,
eventId: prev.eventId,
eventNum: prev.eventNum,
latestEventId: next.eventId,
latestEventNum: next.eventNum,
confirmedEventNum: prev.confirmedEventNum ?? next.confirmedEventNum,
confirmedInBlockNum: prev.confirmedInBlockNum ?? next.confirmedInBlockNum,
createdAtEpochMs: prev.createdAtEpochMs,
updatedAtEpochMs: next.createdAtEpochMs,
};
}
}
function canReplaceEvent(prev, next) {
if (next.content?.kind === RiverTimelineEvent.RedactedEvent && next.content.isAdminRedaction) {
return true;
}
if (next.sender.id === prev.sender.id) {
return true;
}
return false;
}
export function getEditsId(content) {
return content?.kind === RiverTimelineEvent.ChannelMessage ? content.editsEventId : undefined;
}
export function getRedactsId(content) {
return content?.kind === RiverTimelineEvent.RedactionActionEvent
? content.refEventId
: undefined;
}
export function makeRedactionEvent(redactionAction) {
if (redactionAction.content?.kind !== RiverTimelineEvent.RedactionActionEvent) {
throw new Error('makeRedactionEvent called with non-redaction action event');
}
const newContent = {
kind: RiverTimelineEvent.RedactedEvent,
isAdminRedaction: redactionAction.content.adminRedaction,
};
return {
...redactionAction,
content: newContent,
fallbackContent: getFallbackContent('', newContent),
isRedacted: true,
};
}
export function getMessageSenderId(event) {
if (!getChannelMessageContent(event)) {
return undefined;
}
return event.sender.id;
}
export function getChannelMessageContent(event) {
return event?.content?.kind === RiverTimelineEvent.ChannelMessage ? event.content : undefined;
}
//# sourceMappingURL=timelineEvent.js.map