feat: added interaction properties to conversation modal and started UI changes in ConversationListItem based on confirming an interaction

still to to port the original modal checks to use thd convo props
pull/2789/head
William Grant 2 years ago
parent c492fad4ee
commit 8a7007e7c3

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { SessionHtmlRenderer } from '../basic/SessionHTMLRenderer';
import { updateConfirmModal } from '../../state/ducks/modalDialog';
import { updateConfirmModal, updateConfirmModalStatus } from '../../state/ducks/modalDialog';
import { SpacerLG } from '../basic/Text';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { SessionSpinner } from '../basic/SessionSpinner';
@ -9,10 +9,14 @@ import { SessionWrapperModal } from '../SessionWrapperModal';
import { Dispatch } from '@reduxjs/toolkit';
import { shell } from 'electron';
import { MessageInteraction } from '../../interactions';
import {
ConversationInteractionStatus,
ConversationInteractionType,
} from '../../interactions/conversationInteractions';
export type ConfirmationStatus = 'loading' | 'success' | 'error';
// TODO expand support for other confirmation actions
export type ConfirmationType = 'delete-conversation';
// NOTE could be other confirmation statuses and types in future
export type ConfirmationStatus = ConversationInteractionStatus | undefined;
export type ConfirmationType = ConversationInteractionType | undefined;
export interface SessionConfirmDialogProps {
message?: string;
@ -81,10 +85,12 @@ export const SessionConfirm = (props: SessionConfirmDialogProps) => {
await onClickOk();
} catch (e) {
window.log.warn(e);
window.inboxStore?.dispatch(updateConfirmModal({ ...props, status: 'error' }));
window.inboxStore?.dispatch(updateConfirmModalStatus(ConversationInteractionStatus.Error));
} finally {
setIsLoading(false);
window.inboxStore?.dispatch(updateConfirmModal({ ...props, status: 'success' }));
window.inboxStore?.dispatch(
updateConfirmModalStatus(ConversationInteractionStatus.Success)
);
}
}
@ -109,15 +115,22 @@ export const SessionConfirm = (props: SessionConfirmDialogProps) => {
onClickClose();
}
window.inboxStore?.dispatch(updateConfirmModal(null));
// We clear and close the modal but maintain the confirmation status
window.inboxStore?.dispatch(updateConfirmModal({ status: props.status }));
};
useEffect(() => {
if (isLoading) {
window.inboxStore?.dispatch(updateConfirmModal({ ...props, status: 'loading' }));
window.inboxStore?.dispatch(updateConfirmModalStatus(ConversationInteractionStatus.Loading));
}
}, [isLoading]);
useEffect(() => {
window.log.debug(
`WIP: SessionConfirm updating status for ${props.conversationId} to ${props.confirmationType} ${props.status}`
);
}, [props.conversationId, props.confirmationType, props.status]);
return (
<SessionWrapperModal
title={title}

@ -15,6 +15,7 @@ import { updateUserDetailsModal } from '../../../state/ducks/modalDialog';
import _, { isNil } from 'lodash';
import {
useAvatarPath,
useConfirmModalStatusAndType,
useConversationUsername,
useHasUnread,
useIsBlocked,
@ -27,6 +28,7 @@ import { MemoConversationListItemContextMenu } from '../../menu/ConversationList
import { ContextConversationProvider, useConvoIdFromContext } from './ConvoIdContext';
import { ConversationListItemHeaderItem } from './HeaderItem';
import { MessageItem } from './MessageItem';
import { InteractionItem } from './InteractionItem';
// tslint:disable-next-line: no-empty-interface
export type ConversationListItemProps = Pick<ReduxConversationType, 'id'>;
@ -80,6 +82,7 @@ const ConversationListItem = (props: Props) => {
let isBlocked = useIsBlocked(conversationId);
const isSearch = useSelector(isSearching);
const selectedConvo = useSelectedConversationKey();
const confirmModal = useConfirmModalStatusAndType();
const isSelectedConvo = conversationId === selectedConvo && !isNil(selectedConvo);
@ -129,7 +132,11 @@ const ConversationListItem = (props: Props) => {
<AvatarItem />
<div className="module-conversation-list-item__content">
<ConversationListItemHeaderItem />
{confirmModal?.conversationId === conversationId && confirmModal?.type ? (
<InteractionItem {...confirmModal} />
) : (
<MessageItem />
)}
</div>
</div>
<Portal>

@ -0,0 +1,60 @@
import React from 'react';
import { isEmpty } from 'lodash';
import { useIsPrivate, useIsPublic } from '../../../hooks/useParamSelector';
import { MessageBody } from '../../conversation/message/message-content/MessageBody';
import { assertUnreachable } from '../../../types/sqlSharedTypes';
import { ConfirmationStatus } from '../../dialog/SessionConfirm';
import {
ConversationInteractionStatus,
ConversationInteractionType,
} from '../../../interactions/conversationInteractions';
type InteractionItemProps = {
status: ConfirmationStatus | undefined;
type: ConversationInteractionType | undefined;
conversationId: string | undefined;
};
export const InteractionItem = (props: InteractionItemProps) => {
const { status, type, conversationId } = props;
const isGroup = !useIsPrivate(conversationId);
const isCommunity = useIsPublic(conversationId);
if (!type) {
return null;
}
let text = '';
window.log.debug(`WIP: InteractionItem updating status for ${type} ${status}`);
switch (type) {
case ConversationInteractionType.Leave:
const failText = isCommunity
? ''
: isGroup
? window.i18n('leaveGroupFailed')
: window.i18n('deleteConversationFailed');
text =
status === ConversationInteractionStatus.Error
? failText
: status === ConversationInteractionStatus.Loading
? window.i18n('leaving')
: '';
break;
default:
assertUnreachable(type, `MessageItem: Missing case error "${type}"`);
}
if (isEmpty(text)) {
return null;
}
return (
<div className="module-conversation-list-item__message">
<div className="module-conversation-list-item__message__text">
<MessageBody text={text} disableJumbomoji={true} disableLinks={true} isGroup={isGroup} />
</div>
</div>
);
};

@ -3,11 +3,9 @@ import { isEmpty } from 'lodash';
import React from 'react';
import { useSelector } from 'react-redux';
import {
useConfirmModalStatusAndType,
useConversationPropsById,
useHasUnread,
useIsPrivate,
useIsPublic,
useIsTyping,
} from '../../../hooks/useParamSelector';
import { isSearching } from '../../../state/selectors/search';
@ -16,7 +14,6 @@ import { TypingAnimation } from '../../conversation/TypingAnimation';
import { MessageBody } from '../../conversation/message/message-content/MessageBody';
import { OutgoingMessageStatus } from '../../conversation/message/message-content/OutgoingMessageStatus';
import { useConvoIdFromContext } from './ConvoIdContext';
import { assertUnreachable } from '../../../types/sqlSharedTypes';
function useLastMessageFromConvo(convoId: string) {
const convoProps = useConversationPropsById(convoId);
@ -30,7 +27,6 @@ export const MessageItem = () => {
const conversationId = useConvoIdFromContext();
const lastMessage = useLastMessageFromConvo(conversationId);
const isGroup = !useIsPrivate(conversationId);
const isCommunity = useIsPublic(conversationId);
const hasUnread = useHasUnread(conversationId);
const isConvoTyping = useIsTyping(conversationId);
@ -38,37 +34,11 @@ export const MessageItem = () => {
const isSearchingMode = useSelector(isSearching);
const confirmModal = useConfirmModalStatusAndType();
if (!lastMessage && !isConvoTyping) {
return null;
}
let text = lastMessage?.text || '';
if (confirmModal?.conversationId === conversationId && confirmModal?.type) {
window.log.debug(`WIP: updating status for ${confirmModal?.type} ${confirmModal.status}`);
switch (confirmModal?.type) {
case 'delete-conversation':
const failText = isCommunity
? ''
: isGroup
? window.i18n('leaveGroupFailed')
: window.i18n('deleteConversationFailed');
text =
confirmModal.status === 'error'
? failText
: confirmModal.status === 'loading'
? window.i18n('leaving')
: '';
break;
default:
assertUnreachable(
confirmModal?.type,
`MessageItem: Missing case error "${confirmModal?.type}"`
);
}
}
const text = lastMessage?.text || '';
if (isEmpty(text)) {
return null;

@ -44,6 +44,16 @@ import { ReleasedFeatures } from '../util/releaseFeature';
import { Storage, setLastProfileUpdateTimestamp } from '../util/storage';
import { UserGroupsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface';
export enum ConversationInteractionStatus {
Success = 'success',
Error = 'error',
Loading = 'loading',
}
export enum ConversationInteractionType {
Leave = 'leave',
}
export async function copyPublicKeyByConvoId(convoId: string) {
if (OpenGroupUtils.isOpenGroupV2(convoId)) {
const fromWrapper = await UserGroupsWrapperActions.getCommunityByFullUrl(convoId);
@ -237,6 +247,8 @@ export function showLeavePrivateConversationbyConvoId(conversationId: string, na
};
const onClickOk = async () => {
window.log.debug(`WIP: onClickOk ran`);
return;
await getConversationController().delete1o1(conversationId, {
fromSyncMessage: false,
justHidePrivate: true,
@ -252,7 +264,7 @@ export function showLeavePrivateConversationbyConvoId(conversationId: string, na
okText: window.i18n('delete'),
okTheme: SessionButtonColor.Danger,
onClickClose,
confirmationType: 'delete-conversation',
confirmationType: ConversationInteractionType.Leave,
conversationId,
})
);
@ -278,6 +290,8 @@ export function showLeaveGroupByConvoId(conversationId: string, name?: string) {
};
const onClickOk = async () => {
window.log.debug(`WIP: onClickOk ran`);
return;
if (isPublic) {
await getConversationController().deleteCommunity(conversation.id, {
fromSyncMessage: false,
@ -293,7 +307,7 @@ export function showLeaveGroupByConvoId(conversationId: string, name?: string) {
// TODO Communities don't need confirmation modal and have different logic
if (isPublic || (isClosedGroup && !isAdmin)) {
window.inboxStore?.dispatch(
window?.inboxStore?.dispatch(
updateConfirmModal({
title: isPublic ? window.i18n('leaveCommunity') : window.i18n('leaveGroup'),
message: window.i18n('leaveGroupConfirmation', name ? [name] : undefined),
@ -301,13 +315,11 @@ export function showLeaveGroupByConvoId(conversationId: string, name?: string) {
okText: window.i18n('delete'),
okTheme: SessionButtonColor.Danger,
onClickClose,
confirmationType: 'delete-conversation',
confirmationType: ConversationInteractionType.Leave,
conversationId,
})
);
return;
}
} else {
// TODO use different admin modal from figma with add another admin option
window.inboxStore?.dispatch(
adminLeaveClosedGroup({
@ -315,6 +327,8 @@ export function showLeaveGroupByConvoId(conversationId: string, name?: string) {
})
);
}
}
export function showInviteContactByConvoId(conversationId: string) {
window.inboxStore?.dispatch(updateInviteContactModal({ conversationId }));
}

@ -115,6 +115,10 @@ import {
getSubscriberCountOutsideRedux,
} from '../state/selectors/sogsRoomInfo';
import { markAttributesAsReadIfNeeded } from './messageFactory';
import {
ConversationInteractionStatus,
ConversationInteractionType,
} from '../interactions/conversationInteractions';
type InMemoryConvoInfos = {
mentionedUs: boolean;
@ -368,6 +372,13 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
}
if (this.get('interactionType')) {
toRet.interactionType = this.get('interactionType');
}
if (this.get('interactionStatus')) {
toRet.interactionStatus = this.get('interactionStatus');
}
// -- Handle the field stored only in memory for all types of conversation--
const inMemoryConvoInfo = inMemoryConvoInfos.get(this.id);
if (inMemoryConvoInfo) {
@ -2173,6 +2184,26 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return [];
}
public getInteractionState() {
return { type: this.get('interactionType'), status: this.get('interactionStatus') };
}
public async setInteractionState({
type,
status,
shouldCommit,
}: {
type: ConversationInteractionType;
status: ConversationInteractionStatus;
shouldCommit: boolean;
}) {
this.set({ interactionType: type, interactionStatus: status });
if (shouldCommit) {
await this.commit();
}
}
}
export async function commitConversationAndRefreshWrapper(id: string) {

@ -1,5 +1,9 @@
import { defaults } from 'lodash';
import { LastMessageStatusType } from '../state/ducks/conversations';
import {
ConversationInteractionStatus,
ConversationInteractionType,
} from '../interactions/conversationInteractions';
/**
* Private chats have always the type `Private`
@ -101,6 +105,10 @@ export interface ConversationAttributes {
didApproveMe: boolean; // if our message request was approved already (or they've sent us a message request/message themselves). If isApproved & didApproveMe, a message request becomes a contact
markedAsUnread: boolean; // Force the conversation as unread even if all the messages are read. Used to highlight a conversation the user wants to check again later, synced.
// the last interaction we had with this conversation e.g. failed to leave a group
interactionType?: ConversationInteractionType; // e.g. Leave
interactionStatus?: ConversationInteractionStatus; // e.g. Error
}
/**

@ -75,6 +75,8 @@ const allowedKeysFormatRowOfConversation = [
'conversationIdOrigin',
'markedAsUnread',
'priority',
'interactionType',
'interactionStatus',
];
// tslint:disable: cyclomatic-complexity
export function formatRowOfConversation(
@ -192,6 +194,8 @@ const allowedKeysOfConversationAttributes = [
'conversationIdOrigin',
'markedAsUnread',
'priority',
'interactionType',
'interactionStatus',
];
/**

@ -100,6 +100,7 @@ const LOKI_SCHEMA_VERSIONS = [
updateToSessionSchemaVersion29,
updateToSessionSchemaVersion30,
updateToSessionSchemaVersion31,
updateToSessionSchemaVersion32,
];
function updateToSessionSchemaVersion1(currentVersion: number, db: BetterSqlite3.Database) {
@ -1832,6 +1833,25 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite
})();
}
function updateToSessionSchemaVersion32(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 32;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.prepare(`ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN interactionType TEXT;`).run();
db.prepare(`ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN interactionStatus TEXT;`).run();
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
export function printTableColumns(table: string, db: BetterSqlite3.Database) {
console.info(db.pragma(`table_info('${table}');`));
}

@ -443,6 +443,8 @@ function saveConversation(data: ConversationAttributes): SaveConversationReturn
conversationIdOrigin,
priority,
markedAsUnread,
interactionType,
interactionStatus,
} = formatted;
const omited = omit(formatted);
@ -492,6 +494,8 @@ function saveConversation(data: ConversationAttributes): SaveConversationReturn
displayNameInProfile,
conversationIdOrigin,
markedAsUnread: toSqliteBoolean(markedAsUnread),
interactionType,
interactionStatus,
});
return fetchConvoMemoryDetails(id);

@ -16,6 +16,10 @@ import {
} from '../../models/messageType';
import { getConversationController } from '../../session/conversations';
import { ReactionList } from '../../types/Reaction';
import {
ConversationInteractionStatus,
ConversationInteractionType,
} from '../../interactions/conversationInteractions';
export type CallNotificationType = 'missed-call' | 'started-call' | 'answered-a-call';
export type PropsForCallNotification = {
@ -263,6 +267,9 @@ export interface ReduxConversationType {
didApproveMe?: boolean;
isMarkedUnread?: boolean;
interactionType?: ConversationInteractionType;
interactionStatus?: ConversationInteractionStatus;
}
export interface NotificationForConvoOption {
@ -1102,3 +1109,5 @@ export async function openConversationToSpecificMessage(args: {
})
);
}
// export async function updateActionConfirmationStatus

Loading…
Cancel
Save