mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
Merge remote-tracking branch 'w/onion-paths' into clearnet
This commit is contained in:
commit
c9d7f4a1ab
69 changed files with 4508 additions and 1282 deletions
File diff suppressed because it is too large
Load diff
|
@ -89,7 +89,7 @@
|
|||
|
||||
{{#isError}}
|
||||
<div id='error' class='step'>
|
||||
<div class='inner error-dialog clearfix'>
|
||||
<div class='clearfix inner error-dialog'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon alert-outline'></span>
|
||||
<div class='header'>{{ errorHeader }}</div>
|
||||
|
@ -128,7 +128,6 @@
|
|||
|
||||
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/whisper_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/session_confirm_view.js'></script>
|
||||
|
||||
<script type='text/javascript' src='js/views/session_inbox_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/identicon_svg_view.js'></script>
|
||||
|
@ -138,17 +137,11 @@
|
|||
|
||||
|
||||
<!-- DIALOGS-->
|
||||
<script type='text/javascript' src='js/views/update_group_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/edit_profile_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/session_change_nickname_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/invite_contacts_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/admin_leave_closed_group_dialog_view.js'></script>
|
||||
<!-- <script type='text/javascript' src='js/views/update_group_dialog_view.js'></script> -->
|
||||
<!-- <script type='text/javascript' src='js/views/invite_contacts_dialog_view.js'></script> -->
|
||||
<!-- <script type='text/javascript' src='js/views/admin_leave_closed_group_dialog_view.js'></script> -->
|
||||
|
||||
<script type='text/javascript' src='js/views/moderators_add_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/moderators_remove_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/user_details_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/password_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/seed_dialog_view.js'></script>
|
||||
<!-- <script type='text/javascript' src='js/views/user_details_dialog_view.js'></script> -->
|
||||
<script type='text/javascript' src='js/views/session_id_reset_view.js'></script>
|
||||
|
||||
<!-- CRYPTO -->
|
||||
|
|
|
@ -94,7 +94,7 @@
|
|||
|
||||
{{#isError}}
|
||||
<div id='error' class='step'>
|
||||
<div class='inner error-dialog clearfix'>
|
||||
<div class='clearfix inner error-dialog'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon alert-outline'></span>
|
||||
<div class='header'>{{ errorHeader }}</div>
|
||||
|
@ -145,6 +145,7 @@
|
|||
<!-- DIALOGS-->
|
||||
<script type='text/javascript' src='js/views/update_group_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/edit_profile_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/onion_status_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/invite_contacts_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/admin_leave_closed_group_dialog_view.js'></script>
|
||||
|
||||
|
|
256
js/background.js
256
js/background.js
|
@ -2,11 +2,8 @@
|
|||
$,
|
||||
_,
|
||||
Backbone,
|
||||
Signal,
|
||||
storage,
|
||||
textsecure,
|
||||
Whisper,
|
||||
libsignal,
|
||||
BlockedNumberController,
|
||||
*/
|
||||
|
||||
|
@ -315,151 +312,10 @@
|
|||
window.addEventListener('focus', () => Whisper.Notifications.clear());
|
||||
window.addEventListener('unload', () => Whisper.Notifications.fastClear());
|
||||
|
||||
window.confirmationDialog = params => {
|
||||
const confirmDialog = new Whisper.SessionConfirmView({
|
||||
el: $('body'),
|
||||
title: params.title,
|
||||
message: params.message,
|
||||
messageSub: params.messageSub || undefined,
|
||||
resolve: params.resolve || undefined,
|
||||
reject: params.reject || undefined,
|
||||
okText: params.okText || undefined,
|
||||
okTheme: params.okTheme || undefined,
|
||||
closeTheme: params.closeTheme || undefined,
|
||||
cancelText: params.cancelText || undefined,
|
||||
hideCancel: params.hideCancel || false,
|
||||
sessionIcon: params.sessionIcon || undefined,
|
||||
iconSize: params.iconSize || undefined,
|
||||
});
|
||||
|
||||
confirmDialog.render();
|
||||
};
|
||||
|
||||
window.showNicknameDialog = params => {
|
||||
if (appView) {
|
||||
appView.showNicknameDialog(params);
|
||||
}
|
||||
};
|
||||
|
||||
window.showResetSessionIdDialog = () => {
|
||||
appView.showResetSessionIdDialog();
|
||||
};
|
||||
|
||||
window.showEditProfileDialog = async () => {
|
||||
const ourNumber = window.storage.get('primaryDevicePubKey');
|
||||
const conversation = await window
|
||||
.getConversationController()
|
||||
.getOrCreateAndWait(ourNumber, 'private');
|
||||
|
||||
const readFile = attachment =>
|
||||
new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = e => {
|
||||
const data = e.target.result;
|
||||
resolve({
|
||||
...attachment,
|
||||
data,
|
||||
size: data.byteLength,
|
||||
});
|
||||
};
|
||||
fileReader.onerror = reject;
|
||||
fileReader.onabort = reject;
|
||||
fileReader.readAsArrayBuffer(attachment.file);
|
||||
});
|
||||
|
||||
const avatarPath = conversation.getAvatarPath();
|
||||
const profile = conversation.getLokiProfile();
|
||||
const displayName = profile && profile.displayName;
|
||||
|
||||
if (appView) {
|
||||
appView.showEditProfileDialog({
|
||||
profileName: displayName,
|
||||
pubkey: ourNumber,
|
||||
avatarPath,
|
||||
onOk: async (newName, avatar) => {
|
||||
let newAvatarPath = '';
|
||||
let fileUrl = null;
|
||||
let profileKey = null;
|
||||
if (avatar) {
|
||||
const data = await readFile({ file: avatar });
|
||||
// Ensure that this file is either small enough or is resized to meet our
|
||||
// requirements for attachments
|
||||
try {
|
||||
const withBlob = await window.Signal.Util.AttachmentUtil.autoScale(
|
||||
{
|
||||
contentType: avatar.type,
|
||||
file: new Blob([data.data], {
|
||||
type: avatar.contentType,
|
||||
}),
|
||||
},
|
||||
{
|
||||
maxSide: 640,
|
||||
maxSize: 1000 * 1024,
|
||||
}
|
||||
);
|
||||
const dataResized = await window.Signal.Types.Attachment.arrayBufferFromFile(
|
||||
withBlob.file
|
||||
);
|
||||
|
||||
// For simplicity we use the same attachment pointer that would send to
|
||||
// others, which means we need to wait for the database response.
|
||||
// To avoid the wait, we create a temporary url for the local image
|
||||
// and use it until we the the response from the server
|
||||
const tempUrl = window.URL.createObjectURL(avatar);
|
||||
conversation.setLokiProfile({ displayName: newName });
|
||||
conversation.set('avatar', tempUrl);
|
||||
|
||||
// Encrypt with a new key every time
|
||||
profileKey = libsignal.crypto.getRandomBytes(32);
|
||||
const encryptedData = await textsecure.crypto.encryptProfile(
|
||||
dataResized,
|
||||
profileKey
|
||||
);
|
||||
|
||||
const avatarPointer = await window.Fsv2.uploadFileToFsV2(encryptedData);
|
||||
|
||||
({ fileUrl } = avatarPointer);
|
||||
|
||||
storage.put('profileKey', profileKey);
|
||||
|
||||
conversation.set('avatarPointer', fileUrl);
|
||||
|
||||
const upgraded = await Signal.Migrations.processNewAttachment({
|
||||
isRaw: true,
|
||||
data: data.data,
|
||||
url: fileUrl,
|
||||
});
|
||||
newAvatarPath = upgraded.path;
|
||||
// Replace our temporary image with the attachment pointer from the server:
|
||||
conversation.set('avatar', null);
|
||||
conversation.setLokiProfile({
|
||||
displayName: newName,
|
||||
avatar: newAvatarPath,
|
||||
});
|
||||
await conversation.commit();
|
||||
window.libsession.Utils.UserUtils.setLastProfileUpdateTimestamp(Date.now());
|
||||
await window.libsession.Utils.SyncUtils.forceSyncConfigurationNowIfNeeded(true);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'showEditProfileDialog Error ensuring that image is properly sized:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// do not update the avatar if it did not change
|
||||
conversation.setLokiProfile({
|
||||
displayName: newName,
|
||||
});
|
||||
// might be good to not trigger a sync if the name did not change
|
||||
await conversation.commit();
|
||||
window.libsession.Utils.UserUtils.setLastProfileUpdateTimestamp(Date.now());
|
||||
await window.libsession.Utils.SyncUtils.forceSyncConfigurationNowIfNeeded(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Set user's launch count.
|
||||
const prevLaunchCount = window.getSettingValue('launch-count');
|
||||
const launchCount = !prevLaunchCount ? 1 : prevLaunchCount + 1;
|
||||
|
@ -509,40 +365,40 @@
|
|||
window.setMediaPermissions(!value);
|
||||
};
|
||||
|
||||
Whisper.events.on('updateGroupName', async groupConvo => {
|
||||
if (appView) {
|
||||
appView.showUpdateGroupNameDialog(groupConvo);
|
||||
}
|
||||
});
|
||||
Whisper.events.on('updateGroupMembers', async groupConvo => {
|
||||
if (appView) {
|
||||
appView.showUpdateGroupMembersDialog(groupConvo);
|
||||
}
|
||||
});
|
||||
// Whisper.events.on('updateGroupName', async groupConvo => {
|
||||
// if (appView) {
|
||||
// appView.showUpdateGroupNameDialog(groupConvo);
|
||||
// }
|
||||
// });
|
||||
// Whisper.events.on('updateGroupMembers', async groupConvo => {
|
||||
// if (appView) {
|
||||
// appView.showUpdateGroupMembersDialog(groupConvo);
|
||||
// }
|
||||
// });
|
||||
|
||||
Whisper.events.on('inviteContacts', async groupConvo => {
|
||||
if (appView) {
|
||||
appView.showInviteContactsDialog(groupConvo);
|
||||
}
|
||||
});
|
||||
// Whisper.events.on('inviteContacts', async groupConvo => {
|
||||
// if (appView) {
|
||||
// appView.showInviteContactsDialog(groupConvo);
|
||||
// }
|
||||
// });
|
||||
|
||||
Whisper.events.on('addModerators', async groupConvo => {
|
||||
if (appView) {
|
||||
appView.showAddModeratorsDialog(groupConvo);
|
||||
}
|
||||
});
|
||||
// Whisper.events.on('addModerators', async groupConvo => {
|
||||
// if (appView) {
|
||||
// appView.showAddModeratorsDialog(groupConvo);
|
||||
// }
|
||||
// });
|
||||
|
||||
Whisper.events.on('removeModerators', async groupConvo => {
|
||||
if (appView) {
|
||||
appView.showRemoveModeratorsDialog(groupConvo);
|
||||
}
|
||||
});
|
||||
// Whisper.events.on('removeModerators', async groupConvo => {
|
||||
// if (appView) {
|
||||
// appView.showRemoveModeratorsDialog(groupConvo);
|
||||
// }
|
||||
// });
|
||||
|
||||
Whisper.events.on('leaveClosedGroup', async groupConvo => {
|
||||
if (appView) {
|
||||
appView.showLeaveGroupDialog(groupConvo);
|
||||
}
|
||||
});
|
||||
// Whisper.events.on('leaveClosedGroup', async groupConvo => {
|
||||
// if (appView) {
|
||||
// appView.showLeaveGroupDialog(groupConvo);
|
||||
// }
|
||||
// });
|
||||
|
||||
Whisper.Notifications.on('click', (id, messageId) => {
|
||||
window.showWindow();
|
||||
|
@ -561,40 +417,28 @@
|
|||
});
|
||||
});
|
||||
|
||||
Whisper.events.on('onShowUserDetails', async ({ userPubKey }) => {
|
||||
const conversation = await window
|
||||
.getConversationController()
|
||||
.getOrCreateAndWait(userPubKey, 'private');
|
||||
// Whisper.events.on('onShowUserDetails', async ({ userPubKey }) => {
|
||||
// const conversation = await window
|
||||
// .getConversationController()
|
||||
// .getOrCreateAndWait(userPubKey, 'private');
|
||||
|
||||
const avatarPath = conversation.getAvatarPath();
|
||||
const profile = conversation.getLokiProfile();
|
||||
const displayName = profile && profile.displayName;
|
||||
// const avatarPath = conversation.getAvatarPath();
|
||||
// const profile = conversation.getLokiProfile();
|
||||
// const displayName = profile && profile.displayName;
|
||||
|
||||
if (appView) {
|
||||
appView.showUserDetailsDialog({
|
||||
profileName: displayName,
|
||||
pubkey: userPubKey,
|
||||
avatarPath,
|
||||
onStartConversation: () => {
|
||||
window.inboxStore.dispatch(
|
||||
window.actionsCreators.openConversationExternal(conversation.id)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Whisper.events.on('showSeedDialog', async () => {
|
||||
if (appView) {
|
||||
appView.showSeedDialog();
|
||||
}
|
||||
});
|
||||
|
||||
Whisper.events.on('showPasswordDialog', async options => {
|
||||
if (appView) {
|
||||
appView.showPasswordDialog(options);
|
||||
}
|
||||
});
|
||||
// if (appView) {
|
||||
// appView.showUserDetailsDialog({
|
||||
// profileName: displayName,
|
||||
// pubkey: userPubKey,
|
||||
// avatarPath,
|
||||
// onStartConversation: () => {
|
||||
// window.inboxStore.dispatch(
|
||||
// window.actionsCreators.openConversationExternal(conversation.id)
|
||||
// );
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
Whisper.events.on('password-updated', () => {
|
||||
if (appView && appView.inboxView) {
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
this.applyRtl();
|
||||
this.applyHideMenu();
|
||||
|
||||
this.showSeedDialog = this.showSeedDialog.bind(this);
|
||||
this.showPasswordDialog = this.showPasswordDialog.bind(this);
|
||||
},
|
||||
events: {
|
||||
openInbox: 'openInbox',
|
||||
|
@ -113,110 +111,85 @@
|
|||
window.focus(); // FIXME
|
||||
return Promise.resolve();
|
||||
},
|
||||
showEditProfileDialog(options) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
options.theme = this.getThemeObject();
|
||||
const dialog = new Whisper.EditProfileDialogView(options);
|
||||
this.el.prepend(dialog.el);
|
||||
},
|
||||
showNicknameDialog(options) {
|
||||
// // eslint-disable-next-line no-param-reassign
|
||||
const modifiedOptions = { ...options };
|
||||
modifiedOptions.theme = this.getThemeObject();
|
||||
const dialog = new Whisper.SessionNicknameDialog(modifiedOptions);
|
||||
this.el.prepend(dialog.el);
|
||||
},
|
||||
showResetSessionIdDialog() {
|
||||
const theme = this.getThemeObject();
|
||||
const resetSessionIDDialog = new Whisper.SessionIDResetDialog({ theme });
|
||||
|
||||
this.el.prepend(resetSessionIDDialog.el);
|
||||
},
|
||||
showUserDetailsDialog(options) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
options.theme = this.getThemeObject();
|
||||
const dialog = new Whisper.UserDetailsDialogView(options);
|
||||
this.el.prepend(dialog.el);
|
||||
},
|
||||
showPasswordDialog(options) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
options.theme = this.getThemeObject();
|
||||
const dialog = new Whisper.PasswordDialogView(options);
|
||||
this.el.prepend(dialog.el);
|
||||
},
|
||||
showSeedDialog() {
|
||||
const dialog = new Whisper.SeedDialogView({
|
||||
theme: this.getThemeObject(),
|
||||
});
|
||||
this.el.prepend(dialog.el);
|
||||
},
|
||||
// showUserDetailsDialog(options) {
|
||||
// // eslint-disable-next-line no-param-reassign
|
||||
// options.theme = this.getThemeObject();
|
||||
// const dialog = new Whisper.UserDetailsDialogView(options);
|
||||
// this.el.prepend(dialog.el);
|
||||
// },
|
||||
getThemeObject() {
|
||||
const themeSettings = storage.get('theme-setting') || 'light';
|
||||
const theme = themeSettings === 'light' ? window.lightTheme : window.darkTheme;
|
||||
return theme;
|
||||
},
|
||||
showUpdateGroupNameDialog(groupConvo) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
groupConvo.theme = this.getThemeObject();
|
||||
// showUpdateGroupNameDialog(groupConvo) {
|
||||
// // eslint-disable-next-line no-param-reassign
|
||||
// groupConvo.theme = this.getThemeObject();
|
||||
|
||||
const dialog = new Whisper.UpdateGroupNameDialogView(groupConvo);
|
||||
this.el.append(dialog.el);
|
||||
},
|
||||
showUpdateGroupMembersDialog(groupConvo) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
groupConvo.theme = this.getThemeObject();
|
||||
// const dialog = new Whisper.UpdateGroupNameDialogView(groupConvo);
|
||||
// this.el.append(dialog.el);
|
||||
// },
|
||||
// showUpdateGroupMembersDialog(groupConvo) {
|
||||
// // eslint-disable-next-line no-param-reassign
|
||||
// groupConvo.theme = this.getThemeObject();
|
||||
|
||||
const dialog = new Whisper.UpdateGroupMembersDialogView(groupConvo);
|
||||
this.el.append(dialog.el);
|
||||
},
|
||||
showLeaveGroupDialog(groupConvo) {
|
||||
if (!groupConvo.isGroup()) {
|
||||
throw new Error('showLeaveGroupDialog() called with a non group convo.');
|
||||
}
|
||||
// const dialog = new Whisper.UpdateGroupMembersDialogView(groupConvo);
|
||||
// this.el.append(dialog.el);
|
||||
// },
|
||||
// showLeaveGroupDialog(groupConvo) {
|
||||
// if (!groupConvo.isGroup()) {
|
||||
// throw new Error('showLeaveGroupDialog() called with a non group convo.');
|
||||
// }
|
||||
|
||||
const title = i18n('leaveGroup');
|
||||
const message = i18n('leaveGroupConfirmation');
|
||||
const ourPK = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache();
|
||||
const isAdmin = (groupConvo.get('groupAdmins') || []).includes(ourPK);
|
||||
const isClosedGroup = groupConvo.get('is_medium_group') || false;
|
||||
// const title = i18n('leaveGroup');
|
||||
// const message = i18n('leaveGroupConfirmation');
|
||||
// const ourPK = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache();
|
||||
// const isAdmin = (groupConvo.get('groupAdmins') || []).includes(ourPK);
|
||||
// const isClosedGroup = groupConvo.get('is_medium_group') || false;
|
||||
|
||||
// if this is not a closed group, or we are not admin, we can just show a confirmation dialog
|
||||
if (!isClosedGroup || (isClosedGroup && !isAdmin)) {
|
||||
window.confirmationDialog({
|
||||
title,
|
||||
message,
|
||||
resolve: () => groupConvo.leaveClosedGroup(),
|
||||
theme: this.getThemeObject(),
|
||||
});
|
||||
} else {
|
||||
// we are the admin on a closed group. We have to warn the user about the group Deletion
|
||||
this.showAdminLeaveClosedGroupDialog(groupConvo);
|
||||
}
|
||||
},
|
||||
showInviteContactsDialog(groupConvo) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
groupConvo.theme = this.getThemeObject();
|
||||
const dialog = new Whisper.InviteContactsDialogView(groupConvo);
|
||||
this.el.append(dialog.el);
|
||||
},
|
||||
// // if this is not a closed group, or we are not admin, we can just show a confirmation dialog
|
||||
// if (!isClosedGroup || (isClosedGroup && !isAdmin)) {
|
||||
// window.confirmationDialog({
|
||||
// title,
|
||||
// message,
|
||||
// resolve: () => groupConvo.leaveClosedGroup(),
|
||||
// theme: this.getThemeObject(),
|
||||
// });
|
||||
// } else {
|
||||
// // we are the admin on a closed group. We have to warn the user about the group Deletion
|
||||
// this.showAdminLeaveClosedGroupDialog(groupConvo);
|
||||
// }
|
||||
// },
|
||||
// showInviteContactsDialog(groupConvo) {
|
||||
// // eslint-disable-next-line no-param-reassign
|
||||
// groupConvo.theme = this.getThemeObject();
|
||||
// const dialog = new Whisper.InviteContactsDialogView(groupConvo);
|
||||
// this.el.append(dialog.el);
|
||||
// },
|
||||
|
||||
showAdminLeaveClosedGroupDialog(groupConvo) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
groupConvo.theme = this.getThemeObject();
|
||||
const dialog = new Whisper.AdminLeaveClosedGroupDialog(groupConvo);
|
||||
this.el.append(dialog.el);
|
||||
},
|
||||
showAddModeratorsDialog(groupConvo) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
groupConvo.theme = this.getThemeObject();
|
||||
const dialog = new Whisper.AddModeratorsDialogView(groupConvo);
|
||||
this.el.append(dialog.el);
|
||||
},
|
||||
showRemoveModeratorsDialog(groupConvo) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
groupConvo.theme = this.getThemeObject();
|
||||
const dialog = new Whisper.RemoveModeratorsDialogView(groupConvo);
|
||||
this.el.append(dialog.el);
|
||||
},
|
||||
// showAdminLeaveClosedGroupDialog(groupConvo) {
|
||||
// // eslint-disable-next-line no-param-reassign
|
||||
// groupConvo.theme = this.getThemeObject();
|
||||
// const dialog = new Whisper.AdminLeaveClosedGroupDialog(groupConvo);
|
||||
// this.el.append(dialog.el);
|
||||
// },
|
||||
// showAddModeratorsDialog(groupConvo) {
|
||||
// // eslint-disable-next-line no-param-reassign
|
||||
// groupConvo.theme = this.getThemeObject();
|
||||
// const dialog = new Whisper.AddModeratorsDialogView(groupConvo);
|
||||
// this.el.append(dialog.el);
|
||||
// },
|
||||
// showRemoveModeratorsDialog(groupConvo) {
|
||||
// // eslint-disable-next-line no-param-reassign
|
||||
// groupConvo.theme = this.getThemeObject();
|
||||
// const dialog = new Whisper.RemoveModeratorsDialogView(groupConvo);
|
||||
// this.el.append(dialog.el);
|
||||
// },
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
/* global i18n, Whisper */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.EditProfileDialogView = Whisper.View.extend({
|
||||
className: 'loki-dialog modal',
|
||||
initialize({ profileName, avatarPath, pubkey, onOk, theme }) {
|
||||
this.close = this.close.bind(this);
|
||||
|
||||
this.profileName = profileName;
|
||||
this.pubkey = pubkey;
|
||||
this.avatarPath = avatarPath;
|
||||
this.onOk = onOk;
|
||||
this.theme = theme;
|
||||
|
||||
this.$el.focus();
|
||||
this.render();
|
||||
},
|
||||
render() {
|
||||
this.dialogView = new Whisper.ReactWrapperView({
|
||||
className: 'edit-profile-dialog',
|
||||
Component: window.Signal.Components.EditProfileDialog,
|
||||
props: {
|
||||
onOk: this.onOk,
|
||||
onClose: this.close,
|
||||
profileName: this.profileName,
|
||||
pubkey: this.pubkey,
|
||||
avatarPath: this.avatarPath,
|
||||
i18n,
|
||||
theme: this.theme,
|
||||
},
|
||||
});
|
||||
|
||||
this.$el.append(this.dialogView.el);
|
||||
return this;
|
||||
},
|
||||
close() {
|
||||
this.remove();
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -1,57 +0,0 @@
|
|||
/* global Whisper */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.InviteContactsDialogView = Whisper.View.extend({
|
||||
className: 'loki-dialog modal',
|
||||
initialize(convo) {
|
||||
this.close = this.close.bind(this);
|
||||
this.theme = convo.theme;
|
||||
const convos = window.getConversationController().getConversations();
|
||||
|
||||
this.contacts = convos.filter(
|
||||
d => !!d && !d.isBlocked() && d.isPrivate() && !d.isMe() && !!d.get('active_at')
|
||||
);
|
||||
if (!convo.isPublic()) {
|
||||
// filter our zombies and current members from the list of contact we can add
|
||||
|
||||
const members = convo.get('members') || [];
|
||||
const zombies = convo.get('zombies') || [];
|
||||
this.contacts = this.contacts.filter(
|
||||
d => !members.includes(d.id) && !zombies.includes(d.id)
|
||||
);
|
||||
}
|
||||
|
||||
this.chatName = convo.get('name');
|
||||
this.chatServer = convo.get('server');
|
||||
this.isPublic = !!convo.isPublic();
|
||||
this.convo = convo;
|
||||
|
||||
this.$el.focus();
|
||||
this.render();
|
||||
},
|
||||
render() {
|
||||
const view = new Whisper.ReactWrapperView({
|
||||
className: 'invite-friends-dialog',
|
||||
Component: window.Signal.Components.InviteContactsDialog,
|
||||
props: {
|
||||
contactList: this.contacts,
|
||||
onClose: this.close,
|
||||
chatName: this.chatName,
|
||||
theme: this.theme,
|
||||
convo: this.convo,
|
||||
},
|
||||
});
|
||||
|
||||
this.$el.append(view.el);
|
||||
return this;
|
||||
},
|
||||
close() {
|
||||
this.remove();
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -1,44 +0,0 @@
|
|||
/* global Whisper */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.PasswordDialogView = Whisper.View.extend({
|
||||
className: 'loki-dialog password-dialog modal',
|
||||
initialize(options) {
|
||||
this.close = this.close.bind(this);
|
||||
this.onOk = this.onOk.bind(this);
|
||||
this.props = options;
|
||||
|
||||
this.render();
|
||||
},
|
||||
|
||||
render() {
|
||||
this.dialogView = new Whisper.ReactWrapperView({
|
||||
className: 'password-dialog-wrapper',
|
||||
Component: window.Signal.Components.SessionPasswordModal,
|
||||
props: {
|
||||
onClose: this.close,
|
||||
onOk: this.onOk,
|
||||
...this.props,
|
||||
},
|
||||
});
|
||||
|
||||
this.$el.append(this.dialogView.el);
|
||||
return this;
|
||||
},
|
||||
|
||||
onOk(action) {
|
||||
if (this.props.onSuccess) {
|
||||
this.props.onSuccess(action);
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
this.remove();
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -1,35 +0,0 @@
|
|||
/* global Whisper */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.SeedDialogView = Whisper.View.extend({
|
||||
className: 'loki-dialog seed-dialog modal',
|
||||
initialize(options) {
|
||||
this.close = this.close.bind(this);
|
||||
this.theme = options.theme;
|
||||
this.render();
|
||||
},
|
||||
|
||||
render() {
|
||||
this.dialogView = new Whisper.ReactWrapperView({
|
||||
className: 'seed-dialog-wrapper',
|
||||
Component: window.Signal.Components.SessionSeedModal,
|
||||
props: {
|
||||
onClose: this.close,
|
||||
theme: this.theme,
|
||||
},
|
||||
});
|
||||
|
||||
this.$el.append(this.dialogView.el);
|
||||
return this;
|
||||
},
|
||||
|
||||
close() {
|
||||
this.remove();
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -1,54 +0,0 @@
|
|||
/* global Whisper */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.SessionNicknameDialog = Whisper.View.extend({
|
||||
className: 'loki-dialog session-nickname-wrapper modal',
|
||||
initialize(options) {
|
||||
this.props = {
|
||||
title: options.title,
|
||||
message: options.message,
|
||||
onClickOk: this.ok.bind(this),
|
||||
onClickClose: this.cancel.bind(this),
|
||||
convoId: options.convoId,
|
||||
placeholder: options.placeholder,
|
||||
};
|
||||
this.render();
|
||||
},
|
||||
registerEvents() {
|
||||
this.unregisterEvents();
|
||||
document.addEventListener('keyup', this.props.onClickClose, false);
|
||||
},
|
||||
|
||||
unregisterEvents() {
|
||||
document.removeEventListener('keyup', this.props.onClickClose, false);
|
||||
this.$('session-nickname-wrapper').remove();
|
||||
},
|
||||
render() {
|
||||
this.dialogView = new Whisper.ReactWrapperView({
|
||||
className: 'session-nickname-wrapper',
|
||||
Component: window.Signal.Components.SessionNicknameDialog,
|
||||
props: this.props,
|
||||
});
|
||||
|
||||
this.$el.append(this.dialogView.el);
|
||||
return this;
|
||||
},
|
||||
|
||||
close() {
|
||||
this.remove();
|
||||
},
|
||||
cancel() {
|
||||
this.remove();
|
||||
this.unregisterEvents();
|
||||
},
|
||||
ok() {
|
||||
this.remove();
|
||||
this.unregisterEvents();
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -1,80 +0,0 @@
|
|||
/* global Whisper */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.SessionConfirmView = Whisper.View.extend({
|
||||
initialize(options) {
|
||||
this.props = {
|
||||
title: options.title,
|
||||
message: options.message,
|
||||
messageSub: options.messageSub,
|
||||
onClickOk: this.ok.bind(this),
|
||||
onClickClose: this.cancel.bind(this),
|
||||
resolve: options.resolve,
|
||||
reject: options.reject,
|
||||
okText: options.okText,
|
||||
cancelText: options.cancelText,
|
||||
okTheme: options.okTheme,
|
||||
closeTheme: options.closeTheme,
|
||||
hideCancel: options.hideCancel,
|
||||
sessionIcon: options.sessionIcon,
|
||||
iconSize: options.iconSize,
|
||||
};
|
||||
},
|
||||
|
||||
registerEvents() {
|
||||
this.unregisterEvents();
|
||||
document.addEventListener('keyup', this.props.onClickClose, false);
|
||||
},
|
||||
|
||||
unregisterEvents() {
|
||||
document.removeEventListener('keyup', this.props.onClickClose, false);
|
||||
if (this.confirmView && this.confirmView.el) {
|
||||
window.ReactDOM.unmountComponentAtNode(this.confirmView.el);
|
||||
}
|
||||
|
||||
this.$('.session-confirm-wrapper').remove();
|
||||
},
|
||||
|
||||
render() {
|
||||
this.$('.session-confirm-wrapper').remove();
|
||||
this.registerEvents();
|
||||
|
||||
this.confirmView = new Whisper.ReactWrapperView({
|
||||
className: 'loki-dialog modal session-confirm-wrapper',
|
||||
Component: window.Signal.Components.SessionConfirm,
|
||||
props: this.props,
|
||||
});
|
||||
|
||||
this.$el.prepend(this.confirmView.el);
|
||||
},
|
||||
|
||||
ok() {
|
||||
this.unregisterEvents();
|
||||
|
||||
this.$('.session-confirm-wrapper').remove();
|
||||
if (this.props.resolve) {
|
||||
this.props.resolve();
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
this.unregisterEvents();
|
||||
|
||||
this.$('.session-confirm-wrapper').remove();
|
||||
|
||||
if (this.props.reject) {
|
||||
this.props.reject();
|
||||
}
|
||||
},
|
||||
onKeyup(event) {
|
||||
if (event.key === 'Escape' || event.key === 'Esc') {
|
||||
this.unregisterEvents();
|
||||
this.props.onClickClose();
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -59,6 +59,7 @@
|
|||
"classnames": "2.2.5",
|
||||
"color": "^3.1.2",
|
||||
"config": "1.28.1",
|
||||
"country-code-lookup": "^0.0.19",
|
||||
"cross-env": "^6.0.3",
|
||||
"dompurify": "^2.0.7",
|
||||
"electron-is-dev": "^1.1.0",
|
||||
|
@ -72,6 +73,7 @@
|
|||
"fs-extra": "9.0.0",
|
||||
"glob": "7.1.2",
|
||||
"he": "1.2.0",
|
||||
"ip2country": "1.0.1",
|
||||
"jquery": "3.3.1",
|
||||
"jsbn": "1.1.0",
|
||||
"libsodium-wrappers": "^0.7.8",
|
||||
|
@ -117,6 +119,7 @@
|
|||
"to-arraybuffer": "1.0.1",
|
||||
"typings-for-css-modules-loader": "^1.7.0",
|
||||
"underscore": "1.9.0",
|
||||
"use-hooks": "^2.0.0-rc.5",
|
||||
"uuid": "3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
|
||||
.edit-profile-dialog,
|
||||
.create-group-dialog,
|
||||
.user-details-dialog {
|
||||
.user-details-dialog,
|
||||
.onion-status-dialog {
|
||||
.content {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
@ -47,7 +48,7 @@
|
|||
|
||||
.avatar-center-inner {
|
||||
display: flex;
|
||||
padding-top: 30px;
|
||||
// padding-top: 30px;
|
||||
}
|
||||
|
||||
.upload-btn-background {
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 0 20px;
|
||||
z-index: 100;
|
||||
|
@ -302,6 +302,104 @@
|
|||
font-size: $session-font-md;
|
||||
padding: 0px $session-margin-sm;
|
||||
}
|
||||
|
||||
.session-icon-button {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .modal {
|
||||
// position: fixed;
|
||||
// top: 0;
|
||||
// left: 0;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// z-index: 10000;
|
||||
// padding: 2rem;
|
||||
// background-color: rgba(0, 0, 0, 0.55);
|
||||
|
||||
// }
|
||||
|
||||
.onion-status-dialog {
|
||||
.session-modal__header__title {
|
||||
font-size: $session-font-lg;
|
||||
}
|
||||
|
||||
.session-modal {
|
||||
width: $session-modal-size-md;
|
||||
|
||||
&__header {
|
||||
height: 68.45px;
|
||||
}
|
||||
}
|
||||
|
||||
.session-modal__body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
line-height: 1.5em;
|
||||
|
||||
.onionDescriptionContainer {
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.onionPath {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin: 2em auto;
|
||||
|
||||
.dotContainer:not(:last-child) {
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
|
||||
.dotContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
p {
|
||||
margin-bottom: 0 !important;
|
||||
margin-top: 0;
|
||||
margin-left: 2em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dot {
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lineContainer {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
// .line {
|
||||
// height: 100%;
|
||||
// @include themify($themes) {
|
||||
// border-left: 1px solid themed('textColor');
|
||||
// }
|
||||
// display: relative;
|
||||
// // align-self: flex-start;
|
||||
// margin-left: 7px;
|
||||
// // z-index: -1;
|
||||
// }
|
||||
|
||||
// .dot:after {
|
||||
// position: absolute;
|
||||
// left: 0;
|
||||
// top: 0;
|
||||
// content: '';
|
||||
// border-left: 2px solid black;
|
||||
// margin-left: 5px;
|
||||
// height: 100%;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -476,6 +476,7 @@ label {
|
|||
}
|
||||
&__close > div {
|
||||
float: left;
|
||||
margin: 0px;
|
||||
}
|
||||
&__icons > div {
|
||||
float: right;
|
||||
|
@ -1198,6 +1199,35 @@ input {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.onion__description {
|
||||
min-width: 100%;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.onion__node-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: $session-margin-sm;
|
||||
align-items: flex-start;
|
||||
|
||||
.onion__node {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
margin: $session-margin-xs;
|
||||
|
||||
* {
|
||||
margin: $session-margin-sm;
|
||||
}
|
||||
|
||||
.session-icon-button {
|
||||
margin: $session-margin-sm !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.session-nickname-wrapper {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
|
|
|
@ -123,8 +123,9 @@ $session-compose-margin: 20px;
|
|||
cursor: pointer;
|
||||
padding: 30px;
|
||||
|
||||
&:last-child {
|
||||
&:nth-last-child(2) {
|
||||
margin: auto auto 0px auto;
|
||||
opacity: 1 !important;
|
||||
/* Hide theme icon until light theme is ready */
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
$white: #ffffff;
|
||||
$black: #000000;
|
||||
$destructive: #ff453a;
|
||||
$warning: #e7b100;
|
||||
$accentLightTheme: #00e97b;
|
||||
$accentDarkTheme: #00f782;
|
||||
|
||||
|
@ -19,6 +20,7 @@ $themes: (
|
|||
accent: $accentLightTheme,
|
||||
accentButton: $black,
|
||||
cellBackground: #fcfcfc,
|
||||
warning: $warning,
|
||||
destructive: $destructive,
|
||||
modalBackground: #fcfcfc,
|
||||
fakeChatBubbleBackground: #f5f5f5,
|
||||
|
@ -68,6 +70,7 @@ $themes: (
|
|||
dark: (
|
||||
accent: $accentDarkTheme,
|
||||
accentButton: $accentDarkTheme,
|
||||
warning: $warning,
|
||||
destructive: $destructive,
|
||||
cellBackground: #1b1b1b,
|
||||
modalBackground: #101011,
|
||||
|
|
|
@ -137,7 +137,7 @@
|
|||
|
||||
{{#isError}}
|
||||
<div id="error" class="step">
|
||||
<div class="inner error-dialog clearfix">
|
||||
<div class="clearfix inner error-dialog">
|
||||
<div class="step-body">
|
||||
<span class="banner-icon alert-outline"></span>
|
||||
<div class="header">{{ errorHeader }}</div>
|
||||
|
@ -178,7 +178,6 @@
|
|||
|
||||
<script type="text/javascript" src="../js/views/react_wrapper_view.js"></script>
|
||||
<script type="text/javascript" src="../js/views/whisper_view.js"></script>
|
||||
<script type="text/javascript" src="../js/views/session_confirm_view.js"></script>
|
||||
|
||||
<script type='text/javascript' src='../js/views/session_inbox_view.js'></script>
|
||||
<script type="text/javascript" src="../js/views/identicon_svg_view.js"></script>
|
||||
|
@ -190,6 +189,7 @@
|
|||
|
||||
<script type="text/javascript" src="../js/views/update_group_dialog_view.js"></script>
|
||||
<script type="text/javascript" src="../js/views/edit_profile_dialog_view.js"></script>
|
||||
<script type='text/javascript' src='js/views/onion_status_dialog_view.js'></script>
|
||||
<script type="text/javascript" src="../js/views/invite_contacts_dialog_view.js"></script>
|
||||
<script type='text/javascript' src='../js/views/admin_leave_closed_group_dialog_view.js'></script>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { QRCode } from 'react-qr-svg';
|
||||
|
||||
|
@ -7,20 +7,27 @@ import { Avatar, AvatarSize } from './Avatar';
|
|||
import { SessionButton, SessionButtonColor, SessionButtonType } from './session/SessionButton';
|
||||
|
||||
import { SessionIconButton, SessionIconSize, SessionIconType } from './session/icon';
|
||||
import { SessionModal } from './session/SessionModal';
|
||||
import { PillDivider } from './session/PillDivider';
|
||||
import { ToastUtils, UserUtils } from '../session/utils';
|
||||
import { DefaultTheme } from 'styled-components';
|
||||
import { AttachmentUtils, SyncUtils, ToastUtils, UserUtils } from '../session/utils';
|
||||
import { DefaultTheme, useTheme } from 'styled-components';
|
||||
import { MAX_USERNAME_LENGTH } from './session/registration/RegistrationTabs';
|
||||
import { SessionSpinner } from './session/SessionSpinner';
|
||||
import { ConversationTypeEnum } from '../models/conversation';
|
||||
|
||||
import { SessionWrapperModal } from './session/SessionWrapperModal';
|
||||
import { AttachmentUtil, } from '../util';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { ConversationController } from '../session/conversations';
|
||||
|
||||
|
||||
|
||||
interface Props {
|
||||
i18n: any;
|
||||
profileName: string;
|
||||
avatarPath: string;
|
||||
pubkey: string;
|
||||
onClose: any;
|
||||
onOk: any;
|
||||
i18n?: LocalizerType;
|
||||
profileName?: string;
|
||||
avatarPath?: string;
|
||||
pubkey?: string;
|
||||
onClose?: any;
|
||||
onOk?: any;
|
||||
theme: DefaultTheme;
|
||||
}
|
||||
|
||||
|
@ -34,6 +41,7 @@ interface State {
|
|||
|
||||
export class EditProfileDialog extends React.Component<Props, State> {
|
||||
private readonly inputEl: any;
|
||||
private conversationController = ConversationController.getInstance();
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
@ -46,9 +54,9 @@ export class EditProfileDialog extends React.Component<Props, State> {
|
|||
this.fireInputEvent = this.fireInputEvent.bind(this);
|
||||
|
||||
this.state = {
|
||||
profileName: this.props.profileName,
|
||||
setProfileName: this.props.profileName,
|
||||
avatar: this.props.avatarPath,
|
||||
profileName: this.props.profileName || '',
|
||||
setProfileName: this.props.profileName || '',
|
||||
avatar: this.props.avatarPath || '',
|
||||
mode: 'default',
|
||||
loading: false,
|
||||
};
|
||||
|
@ -58,8 +66,44 @@ export class EditProfileDialog extends React.Component<Props, State> {
|
|||
window.addEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async componentDidMount() {
|
||||
const ourNumber = window.storage.get('primaryDevicePubKey');
|
||||
const conversation = await this.conversationController.getOrCreateAndWait(ourNumber, ConversationTypeEnum.PRIVATE);
|
||||
|
||||
const readFile = (attachment: any) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e: any) => {
|
||||
const data = e.target.result;
|
||||
resolve({
|
||||
...attachment,
|
||||
data,
|
||||
size: data.byteLength,
|
||||
});
|
||||
};
|
||||
fileReader.onerror = reject;
|
||||
fileReader.onabort = reject;
|
||||
fileReader.readAsArrayBuffer(attachment.file);
|
||||
});
|
||||
|
||||
const avatarPath = conversation.getAvatarPath();
|
||||
const profile = conversation.getLokiProfile();
|
||||
const displayName = profile && profile.displayName;
|
||||
|
||||
this.setState({
|
||||
...this.state,
|
||||
profileName: profile.profileName,
|
||||
avatar: avatarPath || '',
|
||||
setProfileName: profile.profileName
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
public render() {
|
||||
const i18n = this.props.i18n;
|
||||
// const i18n = this.props.i18n;
|
||||
const i18n = window.i18n;
|
||||
|
||||
const viewDefault = this.state.mode === 'default';
|
||||
const viewEdit = this.state.mode === 'edit';
|
||||
|
@ -70,64 +114,69 @@ export class EditProfileDialog extends React.Component<Props, State> {
|
|||
const backButton =
|
||||
viewEdit || viewQR
|
||||
? [
|
||||
{
|
||||
iconType: SessionIconType.Chevron,
|
||||
iconRotation: 90,
|
||||
onClick: () => {
|
||||
this.setState({ mode: 'default' });
|
||||
},
|
||||
{
|
||||
iconType: SessionIconType.Chevron,
|
||||
iconRotation: 90,
|
||||
onClick: () => {
|
||||
this.setState({ mode: 'default' });
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<SessionModal
|
||||
title={i18n('editProfileModalTitle')}
|
||||
onClose={this.closeDialog}
|
||||
headerReverse={viewEdit || viewQR}
|
||||
headerIconButtons={backButton}
|
||||
theme={this.props.theme}
|
||||
>
|
||||
<div className="spacer-md" />
|
||||
<div className="edit-profile-dialog">
|
||||
|
||||
{viewQR && this.renderQRView(sessionID)}
|
||||
{viewDefault && this.renderDefaultView()}
|
||||
{viewEdit && this.renderEditView()}
|
||||
<SessionWrapperModal
|
||||
title={i18n('editProfileModalTitle')}
|
||||
onClose={this.closeDialog}
|
||||
headerIconButtons={backButton}
|
||||
showExitIcon={true}
|
||||
theme={this.props.theme}
|
||||
>
|
||||
<div className="spacer-md" />
|
||||
|
||||
<div className="session-id-section">
|
||||
<PillDivider text={window.i18n('yourSessionID')} />
|
||||
<p className={classNames('text-selectable', 'session-id-section-display')}>{sessionID}</p>
|
||||
{viewQR && this.renderQRView(sessionID)}
|
||||
{viewDefault && this.renderDefaultView()}
|
||||
{viewEdit && this.renderEditView()}
|
||||
|
||||
<div className="spacer-lg" />
|
||||
<SessionSpinner loading={this.state.loading} />
|
||||
<div className="session-id-section">
|
||||
<PillDivider text={window.i18n('yourSessionID')} />
|
||||
<p className={classNames('text-selectable', 'session-id-section-display')}>{sessionID}</p>
|
||||
|
||||
{viewDefault || viewQR ? (
|
||||
<SessionButton
|
||||
text={window.i18n('editMenuCopy')}
|
||||
buttonType={SessionButtonType.BrandOutline}
|
||||
buttonColor={SessionButtonColor.Green}
|
||||
onClick={() => {
|
||||
this.copySessionID(sessionID);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
!this.state.loading && (
|
||||
<div className="spacer-lg" />
|
||||
<SessionSpinner loading={this.state.loading} />
|
||||
|
||||
{viewDefault || viewQR ? (
|
||||
<SessionButton
|
||||
text={window.i18n('save')}
|
||||
text={window.i18n('editMenuCopy')}
|
||||
buttonType={SessionButtonType.BrandOutline}
|
||||
buttonColor={SessionButtonColor.Green}
|
||||
onClick={this.onClickOK}
|
||||
disabled={this.state.loading}
|
||||
onClick={() => {
|
||||
this.copySessionID(sessionID);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
) : (
|
||||
!this.state.loading && (
|
||||
<SessionButton
|
||||
text={window.i18n('save')}
|
||||
buttonType={SessionButtonType.BrandOutline}
|
||||
buttonColor={SessionButtonColor.Green}
|
||||
onClick={this.onClickOK}
|
||||
disabled={this.state.loading}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
<div className="spacer-lg" />
|
||||
</div>
|
||||
</SessionModal>
|
||||
<div className="spacer-lg" />
|
||||
</div>
|
||||
</SessionWrapperModal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private renderProfileHeader() {
|
||||
return (
|
||||
<>
|
||||
|
@ -170,12 +219,13 @@ export class EditProfileDialog extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
private renderDefaultView() {
|
||||
const name = this.state.setProfileName ? this.state.setProfileName : this.state.profileName;
|
||||
return (
|
||||
<>
|
||||
{this.renderProfileHeader()}
|
||||
|
||||
<div className="profile-name-uneditable">
|
||||
<p>{this.state.setProfileName}</p>
|
||||
<p>{name}</p>
|
||||
<SessionIconButton
|
||||
iconType={SessionIconType.Pencil}
|
||||
iconSize={SessionIconSize.Medium}
|
||||
|
@ -255,6 +305,7 @@ export class EditProfileDialog extends React.Component<Props, State> {
|
|||
switch (event.key) {
|
||||
case 'Enter':
|
||||
if (this.state.mode === 'edit') {
|
||||
// this.onClickOK();
|
||||
this.onClickOK();
|
||||
}
|
||||
break;
|
||||
|
@ -271,8 +322,12 @@ export class EditProfileDialog extends React.Component<Props, State> {
|
|||
ToastUtils.pushCopiedToClipBoard();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tidy the profile name input text and save the new profile name and avatar
|
||||
* @returns
|
||||
*/
|
||||
private onClickOK() {
|
||||
const newName = this.state.profileName.trim();
|
||||
const newName = this.state.profileName ? this.state.profileName.trim() : '';
|
||||
|
||||
if (newName.length === 0 || newName.length > MAX_USERNAME_LENGTH) {
|
||||
return;
|
||||
|
@ -280,9 +335,9 @@ export class EditProfileDialog extends React.Component<Props, State> {
|
|||
|
||||
const avatar =
|
||||
this.inputEl &&
|
||||
this.inputEl.current &&
|
||||
this.inputEl.current.files &&
|
||||
this.inputEl.current.files.length > 0
|
||||
this.inputEl.current &&
|
||||
this.inputEl.current.files &&
|
||||
this.inputEl.current.files.length > 0
|
||||
? this.inputEl.current.files[0]
|
||||
: null;
|
||||
|
||||
|
@ -291,7 +346,7 @@ export class EditProfileDialog extends React.Component<Props, State> {
|
|||
loading: true,
|
||||
},
|
||||
async () => {
|
||||
await this.props.onOk(newName, avatar);
|
||||
await this.commitProfileEdits(newName, avatar);
|
||||
this.setState({
|
||||
loading: false,
|
||||
|
||||
|
@ -307,4 +362,105 @@ export class EditProfileDialog extends React.Component<Props, State> {
|
|||
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
private async commitProfileEdits(newName: string, avatar: any) {
|
||||
let ourNumber = window.storage.get('primaryDevicePubKey');
|
||||
const conversation = await this.conversationController.getOrCreateAndWait(ourNumber, ConversationTypeEnum.PRIVATE);
|
||||
|
||||
let newAvatarPath = '';
|
||||
let url: any = null;
|
||||
let profileKey: any = null;
|
||||
if (avatar) {
|
||||
const data = await AttachmentUtil.readFile({ file: avatar });
|
||||
// Ensure that this file is either small enough or is resized to meet our
|
||||
// requirements for attachments
|
||||
try {
|
||||
|
||||
const withBlob = await AttachmentUtil.autoScale(
|
||||
{
|
||||
contentType: avatar.type,
|
||||
file: new Blob([data.data], {
|
||||
type: avatar.contentType,
|
||||
}),
|
||||
},
|
||||
{
|
||||
maxSide: 640,
|
||||
maxSize: 1000 * 1024,
|
||||
}
|
||||
);
|
||||
const dataResized = await window.Signal.Types.Attachment.arrayBufferFromFile(
|
||||
withBlob.file
|
||||
);
|
||||
|
||||
// For simplicity we use the same attachment pointer that would send to
|
||||
// others, which means we need to wait for the database response.
|
||||
// To avoid the wait, we create a temporary url for the local image
|
||||
// and use it until we the the response from the server
|
||||
const tempUrl = window.URL.createObjectURL(avatar);
|
||||
conversation.setLokiProfile({ displayName: newName });
|
||||
conversation.set('avatar', tempUrl);
|
||||
|
||||
// Encrypt with a new key every time
|
||||
profileKey = window.libsignal.crypto.getRandomBytes(32);
|
||||
const encryptedData = await window.textsecure.crypto.encryptProfile(
|
||||
dataResized,
|
||||
profileKey
|
||||
);
|
||||
|
||||
const avatarPointer = await AttachmentUtils.uploadAvatarV1({
|
||||
...dataResized,
|
||||
data: encryptedData,
|
||||
size: encryptedData.byteLength,
|
||||
});
|
||||
|
||||
url = avatarPointer ? avatarPointer.url : null;
|
||||
window.storage.put('profileKey', profileKey);
|
||||
conversation.set('avatarPointer', url);
|
||||
|
||||
const upgraded = await window.Signal.Migrations.processNewAttachment({
|
||||
isRaw: true,
|
||||
data: data.data,
|
||||
url,
|
||||
});
|
||||
newAvatarPath = upgraded.path;
|
||||
// Replace our temporary image with the attachment pointer from the server:
|
||||
conversation.set('avatar', null);
|
||||
conversation.setLokiProfile({
|
||||
displayName: newName,
|
||||
avatar: newAvatarPath,
|
||||
});
|
||||
|
||||
await conversation.commit();
|
||||
UserUtils.setLastProfileUpdateTimestamp(Date.now());
|
||||
await SyncUtils.forceSyncConfigurationNowIfNeeded(true);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'showEditProfileDialog Error ensuring that image is properly sized:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// do not update the avatar if it did not change
|
||||
conversation.setLokiProfile({
|
||||
displayName: newName,
|
||||
});
|
||||
// might be good to not trigger a sync if the name did not change
|
||||
await conversation.commit();
|
||||
UserUtils.setLastProfileUpdateTimestamp(Date.now());
|
||||
await SyncUtils.forceSyncConfigurationNowIfNeeded(true);
|
||||
}
|
||||
|
||||
// inform all your registered public servers
|
||||
// could put load on all the servers
|
||||
// if they just keep changing their names without sending messages
|
||||
// so we could disable this here
|
||||
// or least it enable for the quickest response
|
||||
window.lokiPublicChatAPI.setProfileName(newName);
|
||||
|
||||
if (avatar) {
|
||||
this.conversationController.getConversations()
|
||||
.filter(convo => convo.isPublic())
|
||||
.forEach(convo => convo.trigger('ourAvatarChanged', { url, profileKey }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { ActionsPanel, SectionType } from './session/ActionsPanel';
|
||||
import { LeftPaneMessageSection } from './session/LeftPaneMessageSection';
|
||||
|
@ -86,12 +86,9 @@ const InnerLeftPaneContactSection = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const LeftPaneSettingsSection = () => {
|
||||
return <LeftPaneSettingSection />;
|
||||
};
|
||||
|
||||
const LeftPaneSection = (props: { isExpired: boolean }) => {
|
||||
const LeftPaneSection = (props: { isExpired: boolean, setModal: any}) => {
|
||||
const focusedSection = useSelector(getFocusedSection);
|
||||
const { setModal } = props;
|
||||
|
||||
if (focusedSection === SectionType.Message) {
|
||||
return <InnerLeftPaneMessageSection isExpired={props.isExpired} />;
|
||||
|
@ -101,7 +98,7 @@ const LeftPaneSection = (props: { isExpired: boolean }) => {
|
|||
return <InnerLeftPaneContactSection />;
|
||||
}
|
||||
if (focusedSection === SectionType.Settings) {
|
||||
return <LeftPaneSettingsSection />;
|
||||
return <LeftPaneSettingSection setModal={setModal} />;
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
|
@ -109,14 +106,20 @@ const LeftPaneSection = (props: { isExpired: boolean }) => {
|
|||
export const LeftPane = (props: Props) => {
|
||||
const theme = useSelector(getTheme);
|
||||
|
||||
const [modal, setModal] = useState<any>(null);
|
||||
|
||||
return (
|
||||
<SessionTheme theme={theme}>
|
||||
<div className="module-left-pane-session">
|
||||
<ActionsPanel />
|
||||
<div className="module-left-pane">
|
||||
<LeftPaneSection isExpired={props.isExpired} />
|
||||
<>
|
||||
{ modal ? modal : null}
|
||||
<SessionTheme theme={theme}>
|
||||
<div className="module-left-pane-session">
|
||||
<ActionsPanel />
|
||||
|
||||
<div className="module-left-pane">
|
||||
<LeftPaneSection setModal={setModal} isExpired={props.isExpired} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SessionTheme>
|
||||
</SessionTheme>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -38,7 +38,7 @@ export class MessageView extends React.Component {
|
|||
*/
|
||||
async function createClosedGroup(
|
||||
groupName: string,
|
||||
groupMembers: Array<ContactType>
|
||||
groupMembers: Array<ContactType>,
|
||||
): Promise<boolean> {
|
||||
// Validate groupName and groupMembers length
|
||||
if (groupName.length === 0) {
|
||||
|
|
188
ts/components/OnionStatusDialog.tsx
Normal file
188
ts/components/OnionStatusDialog.tsx
Normal file
|
@ -0,0 +1,188 @@
|
|||
import React from 'react';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import { getTheme } from '../state/selectors/theme';
|
||||
|
||||
import electron from 'electron';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { StateType } from '../state/reducer';
|
||||
import { SessionIcon, SessionIconButton, SessionIconSize, SessionIconType } from './session/icon';
|
||||
const { shell } = electron;
|
||||
|
||||
import { SessionWrapperModal } from '../components/session/SessionWrapperModal';
|
||||
import { Snode } from '../session/onions';
|
||||
|
||||
import ip2country from 'ip2country';
|
||||
import countryLookup from 'country-code-lookup';
|
||||
import { useTheme } from 'styled-components';
|
||||
import { useNetwork } from '../hooks/useNetwork';
|
||||
|
||||
export type OnionPathModalType = {
|
||||
onConfirm?: () => void;
|
||||
onClose?: () => void;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export type StatusLightType = {
|
||||
glowStartDelay: number;
|
||||
glowDuration: number;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
const OnionPathModalInner = (props: any) => {
|
||||
const onionNodes = useSelector((state: StateType) => state.onionPaths.snodePath);
|
||||
const confirmModalState = useSelector((state: StateType) => state);
|
||||
console.log('onion path: ', confirmModalState);
|
||||
const onionPath = onionNodes.path;
|
||||
// including the device and destination in calculation
|
||||
const glowDuration = onionPath.length + 2;
|
||||
|
||||
const nodes = [
|
||||
{
|
||||
label: window.i18n('device'),
|
||||
},
|
||||
...onionNodes.path,
|
||||
,
|
||||
{
|
||||
label: window.i18n('destination'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="onion__description">{window.i18n('onionPathIndicatorDescription')}</p>
|
||||
<div className="onion__node-list">
|
||||
{nodes.map((snode: Snode | any, index: number) => {
|
||||
return (
|
||||
<OnionNodeStatusLight
|
||||
glowDuration={glowDuration}
|
||||
glowStartDelay={index}
|
||||
label={snode.label}
|
||||
snode={snode}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export type OnionNodeStatusLightType = {
|
||||
snode: Snode;
|
||||
label?: string;
|
||||
glowStartDelay: number;
|
||||
glowDuration: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Component containing a coloured status light and an adjacent country label.
|
||||
* @param props
|
||||
* @returns
|
||||
*/
|
||||
export const OnionNodeStatusLight = (props: OnionNodeStatusLightType): JSX.Element => {
|
||||
const { snode, label, glowStartDelay, glowDuration } = props;
|
||||
const theme = useTheme();
|
||||
|
||||
let labelText = label ? label : countryLookup.byIso(ip2country(snode.ip))?.country;
|
||||
if (!labelText) {
|
||||
labelText = window.i18n('unknownCountry');
|
||||
}
|
||||
return (
|
||||
<div className="onion__node">
|
||||
<ModalStatusLight
|
||||
glowDuration={glowDuration}
|
||||
glowStartDelay={glowStartDelay}
|
||||
color={theme.colors.accent}
|
||||
></ModalStatusLight>
|
||||
{labelText ? (
|
||||
<>
|
||||
<div className="onion-node__country">{labelText}</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* An icon with a pulsating glow emission.
|
||||
*/
|
||||
export const ModalStatusLight = (props: StatusLightType) => {
|
||||
const { glowStartDelay, glowDuration, color } = props;
|
||||
const theme = useSelector(getTheme);
|
||||
|
||||
return (
|
||||
<SessionIcon
|
||||
borderRadius={50}
|
||||
iconColor={color}
|
||||
glowDuration={glowDuration}
|
||||
glowStartDelay={glowStartDelay}
|
||||
iconType={SessionIconType.Circle}
|
||||
iconSize={SessionIconSize.Medium}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A status light specifically for the action panel. Color is based on aggregate node states instead of individual onion node state
|
||||
*/
|
||||
export const ActionPanelOnionStatusLight = (props: { isSelected: boolean, handleClick: () => void }) => {
|
||||
const { isSelected, handleClick } = props;
|
||||
|
||||
let iconColor;
|
||||
const theme = useTheme();
|
||||
const firstOnionPath = useSelector((state: StateType) => state.onionPaths.snodePath.path);
|
||||
const hasOnionPath = firstOnionPath.length > 2;
|
||||
|
||||
// Set icon color based on result
|
||||
const red = theme.colors.destructive;
|
||||
const green = theme.colors.accent;
|
||||
const orange = theme.colors.warning;
|
||||
|
||||
|
||||
iconColor = hasOnionPath ? theme.colors.accent : theme.colors.destructive;
|
||||
const onionState = useSelector((state: StateType) => state.onionPaths);
|
||||
|
||||
iconColor = red;
|
||||
const isOnline = useNetwork();
|
||||
if (!(onionState && onionState.snodePath) || !isOnline) {
|
||||
iconColor = red;
|
||||
} else {
|
||||
const onionSnodePath = onionState.snodePath;
|
||||
if (onionState && onionSnodePath && onionSnodePath.path.length > 0) {
|
||||
let onionNodeCount = onionSnodePath.path.length;
|
||||
iconColor = onionNodeCount > 2 ? green : onionNodeCount > 1 ? orange : red;
|
||||
}
|
||||
}
|
||||
|
||||
return <SessionIconButton
|
||||
iconSize={SessionIconSize.Medium}
|
||||
iconType={SessionIconType.Circle}
|
||||
iconColor={iconColor}
|
||||
onClick={handleClick}
|
||||
isSelected={isSelected}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
|
||||
export const OnionPathModal = (props: OnionPathModalType) => {
|
||||
const onConfirm = () => {
|
||||
shell.openExternal('https://getsession.org/faq/#onion-routing');
|
||||
};
|
||||
return (
|
||||
<SessionWrapperModal
|
||||
title={props.title || window.i18n('onionPathIndicatorTitle')}
|
||||
confirmText={props.confirmText || window.i18n('learnMore')}
|
||||
cancelText={props.cancelText || window.i18n('cancel')}
|
||||
onConfirm={onConfirm}
|
||||
onClose={props.onClose}
|
||||
showExitIcon={true}
|
||||
>
|
||||
<OnionPathModalInner {...props}></OnionPathModalInner>
|
||||
</SessionWrapperModal>
|
||||
);
|
||||
};
|
|
@ -5,6 +5,10 @@ import { SessionModal } from './session/SessionModal';
|
|||
import { SessionButton, SessionButtonColor, SessionButtonType } from './session/SessionButton';
|
||||
import { SessionIdEditable } from './session/SessionIdEditable';
|
||||
import { DefaultTheme } from 'styled-components';
|
||||
import { ConversationController } from '../session/conversations';
|
||||
import { ConversationTypeEnum } from '../models/conversation';
|
||||
import { Session } from 'electron';
|
||||
import { SessionWrapperModal } from './session/SessionWrapperModal';
|
||||
|
||||
interface Props {
|
||||
i18n: any;
|
||||
|
@ -12,7 +16,7 @@ interface Props {
|
|||
avatarPath: string;
|
||||
pubkey: string;
|
||||
onClose: any;
|
||||
onStartConversation: any;
|
||||
// onStartConversation: any;
|
||||
theme: DefaultTheme;
|
||||
}
|
||||
|
||||
|
@ -35,7 +39,7 @@ export class UserDetailsDialog extends React.Component<Props, State> {
|
|||
const { i18n } = this.props;
|
||||
|
||||
return (
|
||||
<SessionModal
|
||||
<SessionWrapperModal
|
||||
title={this.props.profileName}
|
||||
onClose={this.closeDialog}
|
||||
theme={this.props.theme}
|
||||
|
@ -43,6 +47,8 @@ export class UserDetailsDialog extends React.Component<Props, State> {
|
|||
<div className="avatar-center">
|
||||
<div className="avatar-center-inner">{this.renderAvatar()}</div>
|
||||
</div>
|
||||
|
||||
<div className="spacer-md"></div>
|
||||
<SessionIdEditable editable={false} text={this.props.pubkey} />
|
||||
|
||||
<div className="session-modal__button-group__center">
|
||||
|
@ -53,7 +59,7 @@ export class UserDetailsDialog extends React.Component<Props, State> {
|
|||
onClick={this.onClickStartConversation}
|
||||
/>
|
||||
</div>
|
||||
</SessionModal>
|
||||
</SessionWrapperModal>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -95,8 +101,14 @@ export class UserDetailsDialog extends React.Component<Props, State> {
|
|||
this.props.onClose();
|
||||
}
|
||||
|
||||
private onClickStartConversation() {
|
||||
this.props.onStartConversation();
|
||||
private async onClickStartConversation() {
|
||||
// this.props.onStartConversation();
|
||||
const conversation = await ConversationController.getInstance().getOrCreateAndWait(this.props.pubkey, ConversationTypeEnum.PRIVATE);
|
||||
|
||||
window.inboxStore?.dispatch(
|
||||
window.actionsCreators.openConversationExternal(conversation.id)
|
||||
);
|
||||
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,52 +2,53 @@ import React from 'react';
|
|||
|
||||
import { SessionModal } from '../session/SessionModal';
|
||||
import { SessionButton, SessionButtonColor } from '../session/SessionButton';
|
||||
import { DefaultTheme } from 'styled-components';
|
||||
import { DefaultTheme, useTheme } from 'styled-components';
|
||||
import { SessionWrapperModal } from '../session/SessionWrapperModal';
|
||||
|
||||
interface Props {
|
||||
groupName: string;
|
||||
onSubmit: any;
|
||||
onSubmit: () => any;
|
||||
onClose: any;
|
||||
theme: DefaultTheme;
|
||||
}
|
||||
|
||||
class AdminLeaveClosedGroupDialogInner extends React.Component<Props> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const AdminLeaveClosedGroupDialogInner = (props: Props) => {
|
||||
|
||||
this.closeDialog = this.closeDialog.bind(this);
|
||||
this.onClickOK = this.onClickOK.bind(this);
|
||||
const { groupName, theme, onSubmit, onClose } = props;
|
||||
|
||||
const titleText = `${window.i18n('leaveGroup')} ${groupName}`;
|
||||
const warningAsAdmin = `${window.i18n('leaveGroupConfirmationAdmin')}`;
|
||||
const okText = window.i18n('leaveAndRemoveForEveryone');
|
||||
const cancelText = window.i18n('cancel');
|
||||
|
||||
const onClickOK = () => {
|
||||
void onSubmit();
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const titleText = `${window.i18n('leaveGroup')} ${this.props.groupName}`;
|
||||
const warningAsAdmin = `${window.i18n('leaveGroupConfirmationAdmin')}`;
|
||||
const okText = window.i18n('leaveAndRemoveForEveryone');
|
||||
|
||||
return (
|
||||
<SessionModal title={titleText} onClose={this.closeDialog} theme={this.props.theme}>
|
||||
<div className="spacer-lg" />
|
||||
<p>{warningAsAdmin}</p>
|
||||
|
||||
<div className="session-modal__button-group">
|
||||
<SessionButton
|
||||
text={okText}
|
||||
onClick={this.onClickOK}
|
||||
buttonColor={SessionButtonColor.Danger}
|
||||
/>
|
||||
</div>
|
||||
</SessionModal>
|
||||
);
|
||||
const closeDialog = () => {
|
||||
onClose();
|
||||
}
|
||||
|
||||
private onClickOK() {
|
||||
this.props.onSubmit();
|
||||
this.closeDialog();
|
||||
}
|
||||
return (
|
||||
<SessionWrapperModal title={titleText} onClose={closeDialog} theme={theme}>
|
||||
|
||||
private closeDialog() {
|
||||
this.props.onClose();
|
||||
}
|
||||
<div className="spacer-lg" />
|
||||
<p>{warningAsAdmin}</p>
|
||||
|
||||
<div className="session-modal__button-group">
|
||||
<SessionButton
|
||||
text={okText}
|
||||
onClick={onClickOK}
|
||||
buttonColor={SessionButtonColor.Danger}
|
||||
/>
|
||||
<SessionButton
|
||||
text={cancelText}
|
||||
onClick={closeDialog}
|
||||
/>
|
||||
</div>
|
||||
</SessionWrapperModal>
|
||||
);
|
||||
}
|
||||
|
||||
export const AdminLeaveClosedGroupDialog = AdminLeaveClosedGroupDialogInner;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { SessionModal } from '../session/SessionModal';
|
||||
import { SessionButton, SessionButtonColor } from '../session/SessionButton';
|
||||
|
@ -10,29 +10,38 @@ import { initiateGroupUpdate } from '../../session/group';
|
|||
import { ConversationModel, ConversationTypeEnum } from '../../models/conversation';
|
||||
import { getCompleteUrlForV2ConvoId } from '../../interactions/conversation';
|
||||
import _ from 'lodash';
|
||||
import autoBind from 'auto-bind';
|
||||
import { VALIDATION } from '../../session/constants';
|
||||
interface Props {
|
||||
contactList: Array<any>;
|
||||
chatName: string;
|
||||
import { SessionWrapperModal } from '../session/SessionWrapperModal';
|
||||
|
||||
type Props = {
|
||||
// contactList: Array<any>;
|
||||
onClose: any;
|
||||
theme: DefaultTheme;
|
||||
convo: ConversationModel;
|
||||
}
|
||||
};
|
||||
|
||||
interface State {
|
||||
contactList: Array<ContactType>;
|
||||
}
|
||||
const InviteContactsDialogInner = (props: Props) => {
|
||||
const { convo, onClose, theme } = props;
|
||||
// tslint:disable-next-line: max-func-body-length
|
||||
|
||||
class InviteContactsDialogInner extends React.Component<Props, State> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
let contacts = ConversationController.getInstance()
|
||||
.getConversations()
|
||||
.filter(d => !!d && !d.isBlocked() && d.isPrivate() && !d.isMe() && !!d.get('active_at'));
|
||||
if (!convo.isPublic()) {
|
||||
// filter our zombies and current members from the list of contact we can add
|
||||
|
||||
autoBind(this);
|
||||
const members = convo.get('members') || [];
|
||||
const zombies = convo.get('zombies') || [];
|
||||
contacts = contacts.filter(d => !members.includes(d.id) && !zombies.includes(d.id));
|
||||
}
|
||||
|
||||
let contacts = this.props.contactList;
|
||||
const chatName = convo.get('name');
|
||||
// const chatServer = convo.get('server');
|
||||
// const channelId = convo.get('channelId');
|
||||
const isPublicConvo = convo.isPublic();
|
||||
|
||||
contacts = contacts.map(d => {
|
||||
const [contactList, setContactList] = useState(
|
||||
contacts.map((d: ConversationModel) => {
|
||||
const lokiProfile = d.getLokiProfile();
|
||||
const nickname = d.getNickname();
|
||||
const name = nickname
|
||||
|
@ -48,79 +57,77 @@ class InviteContactsDialogInner extends React.Component<Props, State> {
|
|||
id: d.id,
|
||||
authorPhoneNumber: d.id,
|
||||
authorProfileName: name,
|
||||
authorAvatarPath: d?.getAvatarPath(),
|
||||
authorAvatarPath: d?.getAvatarPath() || '',
|
||||
selected: false,
|
||||
authorName: name,
|
||||
checkmarked: false,
|
||||
existingMember,
|
||||
};
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
this.state = {
|
||||
contactList: contacts,
|
||||
};
|
||||
const closeDialog = () => {
|
||||
window.removeEventListener('keyup', onKeyUp);
|
||||
onClose();
|
||||
};
|
||||
|
||||
window.addEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
const onClickOK = () => {
|
||||
const selectedContacts = contactList
|
||||
.filter((d: ContactType) => d.checkmarked)
|
||||
.map((d: ContactType) => d.id);
|
||||
|
||||
public render() {
|
||||
const titleText = `${window.i18n('addingContacts')} ${this.props.chatName}`;
|
||||
const cancelText = window.i18n('cancel');
|
||||
const okText = window.i18n('ok');
|
||||
|
||||
const hasContacts = this.state.contactList.length !== 0;
|
||||
|
||||
return (
|
||||
<SessionModal title={titleText} onClose={this.closeDialog} theme={this.props.theme}>
|
||||
<div className="spacer-lg" />
|
||||
|
||||
<div className="contact-selection-list">{this.renderMemberList()}</div>
|
||||
{hasContacts ? null : (
|
||||
<>
|
||||
<div className="spacer-lg" />
|
||||
<p className="no-contacts">{window.i18n('noContactsToAdd')}</p>
|
||||
<div className="spacer-lg" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="spacer-lg" />
|
||||
|
||||
<div className="session-modal__button-group">
|
||||
<SessionButton text={cancelText} onClick={this.closeDialog} />
|
||||
<SessionButton
|
||||
text={okText}
|
||||
disabled={!hasContacts}
|
||||
onClick={this.onClickOK}
|
||||
buttonColor={SessionButtonColor.Green}
|
||||
/>
|
||||
</div>
|
||||
</SessionModal>
|
||||
);
|
||||
}
|
||||
|
||||
private async submitForOpenGroup(pubkeys: Array<string>) {
|
||||
const { convo } = this.props;
|
||||
|
||||
const completeUrl = await getCompleteUrlForV2ConvoId(convo.id);
|
||||
const groupInvitation = {
|
||||
serverAddress: completeUrl,
|
||||
serverName: convo.getName(),
|
||||
};
|
||||
pubkeys.forEach(async pubkeyStr => {
|
||||
const privateConvo = await ConversationController.getInstance().getOrCreateAndWait(
|
||||
pubkeyStr,
|
||||
ConversationTypeEnum.PRIVATE
|
||||
);
|
||||
|
||||
if (privateConvo) {
|
||||
void privateConvo.sendMessage('', null, null, null, groupInvitation);
|
||||
if (selectedContacts.length > 0) {
|
||||
if (isPublicConvo) {
|
||||
void submitForOpenGroup(selectedContacts);
|
||||
} else {
|
||||
void submitForClosedGroup(selectedContacts);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async submitForClosedGroup(pubkeys: Array<string>) {
|
||||
const { convo } = this.props;
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const onKeyUp = (event: any) => {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
onClickOK();
|
||||
break;
|
||||
case 'Esc':
|
||||
case 'Escape':
|
||||
closeDialog();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
window.addEventListener('keyup', onKeyUp);
|
||||
|
||||
const titleText = `${window.i18n('addingContacts')} ${chatName}`;
|
||||
const cancelText = window.i18n('cancel');
|
||||
const okText = window.i18n('ok');
|
||||
|
||||
const hasContacts = contactList.length !== 0;
|
||||
|
||||
const submitForOpenGroup = async (pubkeys: Array<string>) => {
|
||||
if (convo.isOpenGroupV2()) {
|
||||
const completeUrl = await getCompleteUrlForV2ConvoId(convo.id);
|
||||
const groupInvitation = {
|
||||
serverAddress: completeUrl,
|
||||
serverName: convo.getName(),
|
||||
};
|
||||
pubkeys.forEach(async pubkeyStr => {
|
||||
const privateConvo = await ConversationController.getInstance().getOrCreateAndWait(
|
||||
pubkeyStr,
|
||||
ConversationTypeEnum.PRIVATE
|
||||
);
|
||||
|
||||
if (privateConvo) {
|
||||
void privateConvo.sendMessage('', null, null, null, groupInvitation);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const submitForClosedGroup = async (pubkeys: Array<string>) => {
|
||||
// closed group chats
|
||||
const ourPK = UserUtils.getOurPubKeyStrFromCache();
|
||||
// we only care about real members. If a member is currently a zombie we have to be able to add him back
|
||||
|
@ -157,25 +164,13 @@ class InviteContactsDialogInner extends React.Component<Props, State> {
|
|||
undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onClickOK() {
|
||||
const selectedContacts = this.state.contactList.filter(d => d.checkmarked).map(d => d.id);
|
||||
|
||||
if (selectedContacts.length > 0) {
|
||||
if (this.props.convo.isPublic()) {
|
||||
void this.submitForOpenGroup(selectedContacts);
|
||||
} else {
|
||||
void this.submitForClosedGroup(selectedContacts);
|
||||
}
|
||||
}
|
||||
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private renderMemberList() {
|
||||
const members = this.state.contactList;
|
||||
const selectedContacts = this.state.contactList.filter(d => d.checkmarked).map(d => d.id);
|
||||
const renderMemberList = () => {
|
||||
const members = contactList;
|
||||
const selectedContacts = contactList
|
||||
.filter((d: ContactType) => d.checkmarked)
|
||||
.map((d: ContactType) => d.id);
|
||||
|
||||
return members.map((member: ContactType, index: number) => (
|
||||
<SessionMemberListItem
|
||||
|
@ -184,50 +179,53 @@ class InviteContactsDialogInner extends React.Component<Props, State> {
|
|||
index={index}
|
||||
isSelected={selectedContacts.some(m => m === member.id)}
|
||||
onSelect={(selectedMember: ContactType) => {
|
||||
this.onMemberClicked(selectedMember);
|
||||
onMemberClicked(selectedMember);
|
||||
}}
|
||||
onUnselect={(selectedMember: ContactType) => {
|
||||
this.onMemberClicked(selectedMember);
|
||||
onMemberClicked(selectedMember);
|
||||
}}
|
||||
theme={this.props.theme}
|
||||
theme={theme}
|
||||
/>
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
private onKeyUp(event: any) {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
this.onClickOK();
|
||||
break;
|
||||
case 'Esc':
|
||||
case 'Escape':
|
||||
this.closeDialog();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
private onMemberClicked(clickedMember: ContactType) {
|
||||
const updatedContacts = this.state.contactList.map(member => {
|
||||
const onMemberClicked = (clickedMember: ContactType) => {
|
||||
const updatedContacts = contactList.map((member: ContactType) => {
|
||||
if (member.id === clickedMember.id) {
|
||||
return { ...member, checkmarked: !member.checkmarked };
|
||||
} else {
|
||||
return member;
|
||||
}
|
||||
});
|
||||
setContactList(updatedContacts);
|
||||
};
|
||||
|
||||
this.setState(state => {
|
||||
return {
|
||||
...state,
|
||||
contactList: updatedContacts,
|
||||
};
|
||||
});
|
||||
}
|
||||
return (
|
||||
<SessionWrapperModal title={titleText} onClose={closeDialog} theme={props.theme}>
|
||||
<div className="spacer-lg" />
|
||||
|
||||
private closeDialog() {
|
||||
window.removeEventListener('keyup', this.onKeyUp);
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
<div className="contact-selection-list">{renderMemberList()}</div>
|
||||
{hasContacts ? null : (
|
||||
<>
|
||||
<div className="spacer-lg" />
|
||||
<p className="no-contacts">{window.i18n('noContactsToAdd')}</p>
|
||||
<div className="spacer-lg" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="spacer-lg" />
|
||||
|
||||
<div className="session-modal__button-group">
|
||||
<SessionButton text={cancelText} onClick={closeDialog} />
|
||||
<SessionButton
|
||||
text={okText}
|
||||
disabled={!hasContacts}
|
||||
onClick={onClickOK}
|
||||
buttonColor={SessionButtonColor.Green}
|
||||
/>
|
||||
</div>
|
||||
</SessionWrapperModal>
|
||||
);
|
||||
};
|
||||
|
||||
export const InviteContactsDialog = InviteContactsDialogInner;
|
||||
|
|
|
@ -73,6 +73,7 @@ import { PubKey } from '../../session/types';
|
|||
import { MessageRegularProps } from '../../models/messageType';
|
||||
import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch';
|
||||
import { addSenderAsModerator, removeSenderFromModerator } from '../../interactions/message';
|
||||
import { UserDetailsDialog } from '../UserDetailsDialog';
|
||||
|
||||
// Same as MIN_WIDTH in ImageGrid.tsx
|
||||
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
|
||||
|
@ -461,8 +462,8 @@ class MessageInner extends React.PureComponent<MessageRegularProps, State> {
|
|||
conversationType,
|
||||
direction,
|
||||
isPublic,
|
||||
onShowUserDetails,
|
||||
firstMessageOfSeries,
|
||||
updateSessionConversationModal,
|
||||
} = this.props;
|
||||
|
||||
if (collapseMetadata || conversationType !== 'group' || direction === 'outgoing') {
|
||||
|
@ -474,15 +475,26 @@ class MessageInner extends React.PureComponent<MessageRegularProps, State> {
|
|||
return <div style={{ marginInlineEnd: '60px' }} />;
|
||||
}
|
||||
|
||||
const onAvatarClick = () => {
|
||||
updateSessionConversationModal(
|
||||
<UserDetailsDialog
|
||||
i18n={window.i18n}
|
||||
profileName={userName}
|
||||
avatarPath={authorAvatarPath || ''}
|
||||
pubkey={authorPhoneNumber}
|
||||
onClose={() => updateSessionConversationModal(null)}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="module-message__author-avatar">
|
||||
<Avatar
|
||||
avatarPath={authorAvatarPath}
|
||||
name={userName}
|
||||
size={AvatarSize.S}
|
||||
onAvatarClick={() => {
|
||||
onShowUserDetails(authorPhoneNumber);
|
||||
}}
|
||||
onAvatarClick={onAvatarClick}
|
||||
pubkey={authorPhoneNumber}
|
||||
/>
|
||||
{isPublic && isAdmin && (
|
||||
|
|
|
@ -1,48 +1,31 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { SessionButton, SessionButtonColor, SessionButtonType } from '../session/SessionButton';
|
||||
import { PubKey } from '../../session/types';
|
||||
import { ToastUtils } from '../../session/utils';
|
||||
import { SessionModal } from '../session/SessionModal';
|
||||
import { DefaultTheme } from 'styled-components';
|
||||
import { SessionSpinner } from '../session/SessionSpinner';
|
||||
import { Flex } from '../basic/Flex';
|
||||
import { ConversationModel } from '../../models/conversation';
|
||||
import { ApiV2 } from '../../opengroup/opengroupV2';
|
||||
interface Props {
|
||||
import { SessionWrapperModal } from '../session/SessionWrapperModal';
|
||||
|
||||
type Props = {
|
||||
convo: ConversationModel;
|
||||
onClose: any;
|
||||
theme: DefaultTheme;
|
||||
}
|
||||
};
|
||||
|
||||
interface State {
|
||||
inputBoxValue: string;
|
||||
addingInProgress: boolean;
|
||||
firstLoading: boolean;
|
||||
}
|
||||
export const AddModeratorsDialog = (props: Props) => {
|
||||
const { convo, onClose, theme } = props;
|
||||
|
||||
export class AddModeratorsDialog extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const [inputBoxValue, setInputBoxValue] = useState('');
|
||||
const [addingInProgress, setAddingInProgress] = useState(false);
|
||||
|
||||
this.addAsModerator = this.addAsModerator.bind(this);
|
||||
this.onPubkeyBoxChanges = this.onPubkeyBoxChanges.bind(this);
|
||||
|
||||
this.state = {
|
||||
inputBoxValue: '',
|
||||
addingInProgress: false,
|
||||
firstLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.setState({ firstLoading: false });
|
||||
}
|
||||
|
||||
public async addAsModerator() {
|
||||
const addAsModerator = async () => {
|
||||
// if we don't have valid data entered by the user
|
||||
const pubkey = PubKey.from(this.state.inputBoxValue);
|
||||
const pubkey = PubKey.from(inputBoxValue);
|
||||
if (!pubkey) {
|
||||
window?.log?.info('invalid pubkey for adding as moderator:', this.state.inputBoxValue);
|
||||
window.log.info('invalid pubkey for adding as moderator:', inputBoxValue);
|
||||
ToastUtils.pushInvalidPubKey();
|
||||
return;
|
||||
}
|
||||
|
@ -50,13 +33,10 @@ export class AddModeratorsDialog extends React.Component<Props, State> {
|
|||
window?.log?.info(`asked to add moderator: ${pubkey.key}`);
|
||||
|
||||
try {
|
||||
this.setState({
|
||||
addingInProgress: true,
|
||||
});
|
||||
setAddingInProgress(true);
|
||||
let isAdded: any;
|
||||
|
||||
// this is a v2 opengroup
|
||||
const roomInfos = this.props.convo.toOpenGroupV2();
|
||||
const roomInfos = convo.toOpenGroupV2();
|
||||
isAdded = await ApiV2.addModerator(pubkey, roomInfos);
|
||||
|
||||
if (!isAdded) {
|
||||
|
@ -68,60 +48,55 @@ export class AddModeratorsDialog extends React.Component<Props, State> {
|
|||
ToastUtils.pushUserAddedToModerators();
|
||||
|
||||
// clear input box
|
||||
this.setState({
|
||||
inputBoxValue: '',
|
||||
});
|
||||
setInputBoxValue('');
|
||||
}
|
||||
} catch (e) {
|
||||
window?.log?.error('Got error while adding moderator:', e);
|
||||
} finally {
|
||||
this.setState({
|
||||
addingInProgress: false,
|
||||
});
|
||||
setAddingInProgress(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { i18n } = window;
|
||||
const { addingInProgress, inputBoxValue, firstLoading } = this.state;
|
||||
const chatName = this.props.convo.get('name');
|
||||
const { i18n } = window;
|
||||
const chatName = props.convo.get('name');
|
||||
|
||||
const title = `${i18n('addModerators')}: ${chatName}`;
|
||||
const title = `${i18n('addModerators')}: ${chatName}`;
|
||||
|
||||
const renderContent = !firstLoading;
|
||||
|
||||
return (
|
||||
<SessionModal title={title} onClose={() => this.props.onClose()} theme={this.props.theme}>
|
||||
<Flex container={true} flexDirection="column" alignItems="center">
|
||||
{renderContent && (
|
||||
<>
|
||||
<p>Add Moderator:</p>
|
||||
<input
|
||||
type="text"
|
||||
className="module-main-header__search__input"
|
||||
placeholder={i18n('enterSessionID')}
|
||||
dir="auto"
|
||||
onChange={this.onPubkeyBoxChanges}
|
||||
disabled={addingInProgress}
|
||||
value={inputBoxValue}
|
||||
/>
|
||||
<SessionButton
|
||||
buttonType={SessionButtonType.Brand}
|
||||
buttonColor={SessionButtonColor.Primary}
|
||||
onClick={this.addAsModerator}
|
||||
text={i18n('add')}
|
||||
disabled={addingInProgress}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<SessionSpinner loading={addingInProgress || firstLoading} />
|
||||
</Flex>
|
||||
</SessionModal>
|
||||
);
|
||||
}
|
||||
|
||||
private onPubkeyBoxChanges(e: any) {
|
||||
const onPubkeyBoxChanges = (e: any) => {
|
||||
const val = e.target.value;
|
||||
this.setState({ inputBoxValue: val });
|
||||
}
|
||||
}
|
||||
setInputBoxValue(val);
|
||||
};
|
||||
|
||||
return (
|
||||
<SessionWrapperModal
|
||||
showExitIcon={true}
|
||||
title={title}
|
||||
onClose={() => {
|
||||
onClose();
|
||||
}}
|
||||
theme={theme}
|
||||
>
|
||||
<Flex container={true} flexDirection="column" alignItems="center">
|
||||
<p>Add Moderator:</p>
|
||||
<input
|
||||
type="text"
|
||||
className="module-main-header__search__input"
|
||||
placeholder={i18n('enterSessionID')}
|
||||
dir="auto"
|
||||
onChange={onPubkeyBoxChanges}
|
||||
disabled={addingInProgress}
|
||||
value={inputBoxValue}
|
||||
/>
|
||||
<SessionButton
|
||||
buttonType={SessionButtonType.Brand}
|
||||
buttonColor={SessionButtonColor.Primary}
|
||||
onClick={addAsModerator}
|
||||
text={i18n('add')}
|
||||
disabled={addingInProgress}
|
||||
/>
|
||||
|
||||
<SessionSpinner loading={addingInProgress} />
|
||||
</Flex>
|
||||
</SessionWrapperModal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import { ContactType, SessionMemberListItem } from '../session/SessionMemberList
|
|||
import { SessionModal } from '../session/SessionModal';
|
||||
import { SessionSpinner } from '../session/SessionSpinner';
|
||||
import _ from 'lodash';
|
||||
import { SessionWrapperModal } from '../session/SessionWrapperModal';
|
||||
interface Props {
|
||||
convo: ConversationModel;
|
||||
onClose: any;
|
||||
|
@ -54,7 +55,7 @@ export class RemoveModeratorsDialog extends React.Component<Props, State> {
|
|||
const renderContent = !firstLoading;
|
||||
|
||||
return (
|
||||
<SessionModal title={title} onClose={this.closeDialog} theme={this.props.theme}>
|
||||
<SessionWrapperModal title={title} onClose={this.closeDialog} theme={this.props.theme}>
|
||||
<Flex container={true} flexDirection="column" alignItems="center">
|
||||
{renderContent && (
|
||||
<>
|
||||
|
@ -85,7 +86,7 @@ export class RemoveModeratorsDialog extends React.Component<Props, State> {
|
|||
|
||||
<SessionSpinner loading={firstLoading} />
|
||||
</Flex>
|
||||
</SessionModal>
|
||||
</SessionWrapperModal>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { ConversationController } from '../../session/conversations';
|
|||
|
||||
import _ from 'lodash';
|
||||
import { Text } from '../basic/Text';
|
||||
import { SessionWrapperModal } from '../session/SessionWrapperModal';
|
||||
|
||||
interface Props {
|
||||
titleText: string;
|
||||
|
@ -49,8 +50,8 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
|
|||
const name = nickname
|
||||
? nickname
|
||||
: lokiProfile
|
||||
? lokiProfile.displayName
|
||||
: window.i18n('anonymous');
|
||||
? lokiProfile.displayName
|
||||
: window.i18n('anonymous');
|
||||
|
||||
const existingMember = this.props.existingMembers.includes(d.id);
|
||||
|
||||
|
@ -123,7 +124,7 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
|
|||
const hasZombies = Boolean(existingZombies.length);
|
||||
|
||||
return (
|
||||
<SessionModal
|
||||
<SessionWrapperModal
|
||||
title={titleText}
|
||||
// tslint:disable-next-line: no-void-expression
|
||||
onClose={() => this.closeDialog()}
|
||||
|
@ -150,7 +151,7 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
</SessionModal>
|
||||
</SessionWrapperModal>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -228,6 +229,7 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Return members that would comprise the group given the
|
||||
// current state in `users`
|
||||
private getWouldBeMembers(users: Array<ContactType>) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import { SessionModal } from '../session/SessionModal';
|
|||
import { SessionButton, SessionButtonColor } from '../session/SessionButton';
|
||||
import { Avatar, AvatarSize } from '../Avatar';
|
||||
import { DefaultTheme, withTheme } from 'styled-components';
|
||||
import { SessionWrapperModal } from '../session/SessionWrapperModal';
|
||||
|
||||
interface Props {
|
||||
titleText: string;
|
||||
|
@ -78,15 +79,23 @@ class UpdateGroupNameDialogInner extends React.Component<Props, State> {
|
|||
);
|
||||
|
||||
return (
|
||||
<SessionModal
|
||||
<SessionWrapperModal
|
||||
title={titleText}
|
||||
// tslint:disable-next-line: no-void-expression
|
||||
onClose={() => this.closeDialog()}
|
||||
theme={this.props.theme}
|
||||
>
|
||||
<div className="spacer-md" />
|
||||
<p className={errorMessageClasses}>{errorMsg}</p>
|
||||
<div className="spacer-md" />
|
||||
|
||||
{ this.state.errorDisplayed ?
|
||||
<>
|
||||
<div className="spacer-md" />
|
||||
<p className={errorMessageClasses}>{errorMsg}</p>
|
||||
<div className="spacer-md" />
|
||||
</>
|
||||
:
|
||||
null
|
||||
}
|
||||
|
||||
{this.renderAvatar()}
|
||||
<div className="spacer-md" />
|
||||
|
||||
|
@ -94,7 +103,7 @@ class UpdateGroupNameDialogInner extends React.Component<Props, State> {
|
|||
type="text"
|
||||
className="profile-name-input"
|
||||
value={this.state.groupName}
|
||||
placeholder={this.props.i18n('groupNamePlaceholder')}
|
||||
placeholder={window.i18n('groupNamePlaceholder')}
|
||||
onChange={this.onGroupNameChanged}
|
||||
tabIndex={0}
|
||||
required={true}
|
||||
|
@ -112,7 +121,7 @@ class UpdateGroupNameDialogInner extends React.Component<Props, State> {
|
|||
buttonColor={SessionButtonColor.Green}
|
||||
/>
|
||||
</div>
|
||||
</SessionModal>
|
||||
</SessionWrapperModal>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -45,6 +45,10 @@ import { FSv2 } from '../../fileserver';
|
|||
import { debounce } from 'lodash';
|
||||
import { DURATION } from '../../session/constants';
|
||||
import { actions as conversationActions } from '../../state/ducks/conversations';
|
||||
import { ActionPanelOnionStatusLight, OnionPathModal } from '../OnionStatusDialog';
|
||||
import { EditProfileDialog } from '../EditProfileDialog';
|
||||
import { StateType } from '../../state/reducer';
|
||||
import { SessionConfirm } from './SessionConfirm';
|
||||
|
||||
// tslint:disable-next-line: no-import-side-effect no-submodule-imports
|
||||
|
||||
|
@ -55,22 +59,34 @@ export enum SectionType {
|
|||
Channel,
|
||||
Settings,
|
||||
Moon,
|
||||
PathIndicator,
|
||||
}
|
||||
|
||||
const Section = (props: { type: SectionType; avatarPath?: string }) => {
|
||||
const Section = (props: { setModal?: any; type: SectionType; avatarPath?: string }) => {
|
||||
const ourNumber = useSelector(getOurNumber);
|
||||
const unreadMessageCount = useSelector(getUnreadMessageCount);
|
||||
const theme = useSelector(getTheme);
|
||||
const dispatch = useDispatch();
|
||||
const { type, avatarPath } = props;
|
||||
const { setModal, type, avatarPath } = props;
|
||||
|
||||
const focusedSection = useSelector(getFocusedSection);
|
||||
const isSelected = focusedSection === props.type;
|
||||
|
||||
const handleModalClose = () => {
|
||||
setModal(null);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
/* tslint:disable:no-void-expression */
|
||||
if (type === SectionType.Profile) {
|
||||
window.showEditProfileDialog();
|
||||
// window.showEditProfileDialog();
|
||||
|
||||
// window.inboxStore?.dispatch(updateConfirmModal({ title: "title test" }));
|
||||
|
||||
// dispatch(updateConfirmModal({ title: "title test" }));
|
||||
|
||||
// setModal(<EditProfileDialog2 onClose={() => setModal(null)}></EditProfileDialog2>);
|
||||
setModal(<EditProfileDialog onClose={handleModalClose} theme={theme} />);
|
||||
} else if (type === SectionType.Moon) {
|
||||
const themeFromSettings = window.Events.getThemeSetting();
|
||||
const updatedTheme = themeFromSettings === 'dark' ? 'light' : 'dark';
|
||||
|
@ -78,6 +94,9 @@ const Section = (props: { type: SectionType; avatarPath?: string }) => {
|
|||
|
||||
const newThemeObject = updatedTheme === 'dark' ? darkTheme : lightTheme;
|
||||
dispatch(applyTheme(newThemeObject));
|
||||
} else if (type === SectionType.PathIndicator) {
|
||||
// Show Path Indicator Modal
|
||||
setModal(<OnionPathModal onClose={handleModalClose} />);
|
||||
} else {
|
||||
dispatch(clearSearch());
|
||||
dispatch(showLeftPaneSection(type));
|
||||
|
@ -100,6 +119,12 @@ const Section = (props: { type: SectionType; avatarPath?: string }) => {
|
|||
);
|
||||
}
|
||||
|
||||
let iconColor = undefined;
|
||||
if (type === SectionType.PathIndicator) {
|
||||
}
|
||||
|
||||
const unreadToShow = type === SectionType.Message ? unreadMessageCount : undefined;
|
||||
|
||||
let iconType: SessionIconType;
|
||||
switch (type) {
|
||||
case SectionType.Message:
|
||||
|
@ -114,22 +139,26 @@ const Section = (props: { type: SectionType; avatarPath?: string }) => {
|
|||
case SectionType.Moon:
|
||||
iconType = SessionIconType.Moon;
|
||||
break;
|
||||
|
||||
default:
|
||||
iconType = SessionIconType.Moon;
|
||||
}
|
||||
|
||||
const unreadToShow = type === SectionType.Message ? unreadMessageCount : undefined;
|
||||
|
||||
return (
|
||||
<SessionIconButton
|
||||
iconSize={SessionIconSize.Medium}
|
||||
iconType={iconType}
|
||||
notificationCount={unreadToShow}
|
||||
onClick={handleClick}
|
||||
isSelected={isSelected}
|
||||
theme={theme}
|
||||
/>
|
||||
<>
|
||||
{type === SectionType.PathIndicator ? (
|
||||
<ActionPanelOnionStatusLight handleClick={handleClick} isSelected={isSelected} />
|
||||
) : (
|
||||
<SessionIconButton
|
||||
iconSize={SessionIconSize.Medium}
|
||||
iconType={iconType}
|
||||
iconColor={iconColor}
|
||||
notificationCount={unreadToShow}
|
||||
onClick={handleClick}
|
||||
isSelected={isSelected}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -311,8 +340,8 @@ const doAppStartUp = () => {
|
|||
*/
|
||||
export const ActionsPanel = () => {
|
||||
const [startCleanUpMedia, setStartCleanUpMedia] = useState(false);
|
||||
|
||||
const ourPrimaryConversation = useSelector(getOurPrimaryConversation);
|
||||
const [modal, setModal] = useState<any>(null);
|
||||
|
||||
// this maxi useEffect is called only once: when the component is mounted.
|
||||
// For the action panel, it means this is called only one per app start/with a user loggedin
|
||||
|
@ -353,15 +382,42 @@ export const ActionsPanel = () => {
|
|||
void triggerAvatarReUploadIfNeeded();
|
||||
}, DURATION.DAYS * 1);
|
||||
|
||||
return (
|
||||
<div className="module-left-pane__sections-container">
|
||||
<Section type={SectionType.Profile} avatarPath={ourPrimaryConversation.avatarPath} />
|
||||
<Section type={SectionType.Message} />
|
||||
<Section type={SectionType.Contact} />
|
||||
<Section type={SectionType.Settings} />
|
||||
const formatLog = (s: any) => {
|
||||
console.log('@@@@:: ', s);
|
||||
};
|
||||
|
||||
<SessionToastContainer />
|
||||
<Section type={SectionType.Moon} />
|
||||
</div>
|
||||
// const confirmModalState = useSelector((state: StateType) => state);
|
||||
|
||||
const confirmModalState = useSelector((state: StateType) => state.confirmModal);
|
||||
|
||||
console.log('@@@ confirm modal state', confirmModalState);
|
||||
|
||||
// formatLog(confirmModalState.modalState.title);
|
||||
|
||||
formatLog(confirmModalState);
|
||||
|
||||
// formatLog(confirmModalState2);
|
||||
|
||||
return (
|
||||
<>
|
||||
{modal ? modal : null}
|
||||
{/* { confirmModalState && confirmModalState.title ? <div>{confirmModalState.title}</div> : null} */}
|
||||
{confirmModalState ? <SessionConfirm {...confirmModalState} /> : null}
|
||||
<div className="module-left-pane__sections-container">
|
||||
<Section
|
||||
setModal={setModal}
|
||||
type={SectionType.Profile}
|
||||
avatarPath={ourPrimaryConversation.avatarPath}
|
||||
/>
|
||||
<Section type={SectionType.Message} />
|
||||
<Section type={SectionType.Contact} />
|
||||
<Section type={SectionType.Settings} />
|
||||
|
||||
<SessionToastContainer />
|
||||
|
||||
<Section setModal={setModal} type={SectionType.PathIndicator} />
|
||||
<Section type={SectionType.Moon} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -30,6 +30,8 @@ import autoBind from 'auto-bind';
|
|||
import { onsNameRegex } from '../../session/snode_api/SNodeAPI';
|
||||
import { SNodeAPI } from '../../session/snode_api';
|
||||
|
||||
import { createClosedGroup } from "../../receiver/closedGroups";
|
||||
|
||||
export interface Props {
|
||||
searchTerm: string;
|
||||
|
||||
|
@ -60,6 +62,7 @@ interface State {
|
|||
loading: boolean;
|
||||
overlay: false | SessionComposeToType;
|
||||
valuePasted: string;
|
||||
modal: null | JSX.Element;
|
||||
}
|
||||
|
||||
export class LeftPaneMessageSection extends React.Component<Props, State> {
|
||||
|
@ -72,6 +75,7 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
|
|||
loading: false,
|
||||
overlay: false,
|
||||
valuePasted: '',
|
||||
modal: null
|
||||
};
|
||||
|
||||
autoBind(this);
|
||||
|
@ -165,11 +169,20 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
|
|||
return (
|
||||
<div className="session-left-pane-section-content">
|
||||
{this.renderHeader()}
|
||||
{ this.state.modal ? this.state.modal : null }
|
||||
{overlay ? this.renderClosableOverlay(overlay) : this.renderConversations()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public setModal (modal: null | JSX.Element) {
|
||||
this.setState({
|
||||
modal
|
||||
});
|
||||
}
|
||||
|
||||
public renderConversations() {
|
||||
return (
|
||||
<div className="module-conversations-list-content">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { SessionButton, SessionButtonColor, SessionButtonType } from './SessionButton';
|
||||
|
@ -12,6 +12,8 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||
import { showSettingsSection } from '../../state/ducks/section';
|
||||
import { getFocusedSettingsSection } from '../../state/selectors/section';
|
||||
import { getTheme } from '../../state/selectors/theme';
|
||||
import { SessionConfirm } from './SessionConfirm';
|
||||
import { SessionSeedModal } from './SessionSeedModal';
|
||||
|
||||
type Props = {
|
||||
settingsCategory: SessionSettingCategory;
|
||||
|
@ -102,21 +104,45 @@ const LeftPaneSettingsCategories = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const onDeleteAccount = () => {
|
||||
const onDeleteAccount = ( setModal: any) => {
|
||||
const title = window.i18n('clearAllData');
|
||||
const message = window.i18n('deleteAccountWarning');
|
||||
|
||||
window.confirmationDialog({
|
||||
title,
|
||||
message,
|
||||
resolve: deleteAccount,
|
||||
okTheme: 'danger',
|
||||
});
|
||||
const clearModal = () => {
|
||||
setModal(null);
|
||||
}
|
||||
|
||||
setModal(
|
||||
<SessionConfirm
|
||||
title={title}
|
||||
message={message}
|
||||
onClickOk={deleteAccount}
|
||||
okTheme={SessionButtonColor.Danger}
|
||||
onClickClose={clearModal}
|
||||
/>)
|
||||
};
|
||||
|
||||
const LeftPaneBottomButtons = () => {
|
||||
const onShowRecoverPhrase = (setModal: any) => {
|
||||
|
||||
const clearModal = () => {
|
||||
setModal(null);
|
||||
}
|
||||
|
||||
setModal(
|
||||
|
||||
<SessionSeedModal
|
||||
onClose={clearModal}
|
||||
></SessionSeedModal>
|
||||
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
const LeftPaneBottomButtons = (props: { setModal: any}) => {
|
||||
const dangerButtonText = window.i18n('clearAllData');
|
||||
const showRecoveryPhrase = window.i18n('showRecoveryPhrase');
|
||||
const { setModal } = props;
|
||||
|
||||
return (
|
||||
<div className="left-pane-setting-bottom-buttons">
|
||||
|
@ -124,28 +150,29 @@ const LeftPaneBottomButtons = () => {
|
|||
text={dangerButtonText}
|
||||
buttonType={SessionButtonType.SquareOutline}
|
||||
buttonColor={SessionButtonColor.Danger}
|
||||
onClick={onDeleteAccount}
|
||||
onClick={() => onDeleteAccount(setModal)}
|
||||
/>
|
||||
|
||||
<SessionButton
|
||||
text={showRecoveryPhrase}
|
||||
buttonType={SessionButtonType.SquareOutline}
|
||||
buttonColor={SessionButtonColor.White}
|
||||
onClick={() => window.Whisper.events.trigger('showSeedDialog')}
|
||||
onClick={() => onShowRecoverPhrase(setModal)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LeftPaneSettingSection = () => {
|
||||
export const LeftPaneSettingSection = (props: { setModal: any}) => {
|
||||
const theme = useSelector(getTheme);
|
||||
const { setModal } = props;
|
||||
|
||||
return (
|
||||
<div className="left-pane-setting-section">
|
||||
<LeftPaneSectionHeader label={window.i18n('settingsHeader')} theme={theme} />
|
||||
<div className="left-pane-setting-content">
|
||||
<LeftPaneSettingsCategories />
|
||||
<LeftPaneBottomButtons />
|
||||
<LeftPaneBottomButtons setModal={setModal} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import React from 'react';
|
||||
|
||||
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
|
||||
import { SessionToggle } from './SessionToggle';
|
||||
import { SessionIdEditable } from './SessionIdEditable';
|
||||
import { UserSearchDropdown } from './UserSearchDropdown';
|
||||
import { ContactType, SessionMemberListItem } from './SessionMemberListItem';
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
import { SessionButton, SessionButtonColor, SessionButtonType } from './SessionButton';
|
||||
import { SessionSpinner } from './SessionSpinner';
|
||||
import { PillDivider } from './PillDivider';
|
||||
import { DefaultTheme } from 'styled-components';
|
||||
import { UserUtils } from '../../session/utils';
|
||||
import { ConversationTypeEnum } from '../../models/conversation';
|
||||
import { SessionJoinableRooms } from './SessionJoinableDefaultRooms';
|
||||
import { SpacerLG, SpacerMD } from '../basic/Text';
|
||||
|
|
|
@ -3,60 +3,108 @@ import { SessionModal } from './SessionModal';
|
|||
import { SessionButton, SessionButtonColor } from './SessionButton';
|
||||
import { SessionHtmlRenderer } from './SessionHTMLRenderer';
|
||||
import { SessionIcon, SessionIconSize, SessionIconType } from './icon';
|
||||
import { DefaultTheme, withTheme } from 'styled-components';
|
||||
import { DefaultTheme, useTheme, withTheme } from 'styled-components';
|
||||
import { SessionWrapperModal } from './SessionWrapperModal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateConfirmModal } from '../../state/ducks/modalDialog';
|
||||
import { update } from 'lodash';
|
||||
|
||||
type Props = {
|
||||
message: string;
|
||||
messageSub: string;
|
||||
title: string;
|
||||
export interface SessionConfirmDialogProps {
|
||||
message?: string;
|
||||
messageSub?: string;
|
||||
title?: string;
|
||||
onOk?: any;
|
||||
onClose?: any;
|
||||
onClickOk: any;
|
||||
onClickClose: any;
|
||||
onClickOk?: () => any;
|
||||
onClickClose?: () => any;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
hideCancel: boolean;
|
||||
okTheme: SessionButtonColor;
|
||||
closeTheme: SessionButtonColor;
|
||||
hideCancel?: boolean;
|
||||
okTheme?: SessionButtonColor;
|
||||
closeTheme?: SessionButtonColor;
|
||||
sessionIcon?: SessionIconType;
|
||||
iconSize?: SessionIconSize;
|
||||
theme: DefaultTheme;
|
||||
theme?: DefaultTheme;
|
||||
closeAfterClickOk?: boolean;
|
||||
shouldShowConfirm?: () => boolean | undefined;
|
||||
};
|
||||
|
||||
const SessionConfirmInner = (props: Props) => {
|
||||
const SessionConfirmInner = (props: SessionConfirmDialogProps) => {
|
||||
const {
|
||||
title = '',
|
||||
message,
|
||||
message = '',
|
||||
messageSub = '',
|
||||
okTheme = SessionButtonColor.Primary,
|
||||
closeTheme = SessionButtonColor.Primary,
|
||||
onClickOk,
|
||||
onClickClose,
|
||||
closeAfterClickOk = true,
|
||||
hideCancel = false,
|
||||
sessionIcon,
|
||||
iconSize,
|
||||
shouldShowConfirm,
|
||||
// updateConfirmModal
|
||||
} = props;
|
||||
|
||||
const okText = props.okText || window.i18n('ok');
|
||||
const cancelText = props.cancelText || window.i18n('cancel');
|
||||
const showHeader = !!props.title;
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const messageSubText = messageSub ? 'session-confirm-main-message' : 'subtle';
|
||||
|
||||
/**
|
||||
* Calls close function after the ok button is clicked. If no close method specified, closes the modal
|
||||
*/
|
||||
const onClickOkWithClose = () => {
|
||||
if (onClickOk) {
|
||||
onClickOk();
|
||||
}
|
||||
|
||||
if (onClickClose) {
|
||||
onClickClose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const onClickOkHandler = () => {
|
||||
if (onClickOk) {
|
||||
onClickOk();
|
||||
}
|
||||
|
||||
|
||||
window.inboxStore?.dispatch(updateConfirmModal(null));
|
||||
}
|
||||
|
||||
if (shouldShowConfirm && !shouldShowConfirm()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onClickCancelHandler = () => {
|
||||
if (onClickClose) {
|
||||
onClickClose();
|
||||
}
|
||||
|
||||
window.inboxStore?.dispatch(updateConfirmModal(null));
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<SessionModal
|
||||
<SessionWrapperModal
|
||||
title={title}
|
||||
onClose={onClickClose}
|
||||
showExitIcon={false}
|
||||
showHeader={showHeader}
|
||||
theme={props.theme}
|
||||
theme={theme}
|
||||
>
|
||||
|
||||
{!showHeader && <div className="spacer-lg" />}
|
||||
|
||||
<div className="session-modal__centered">
|
||||
{sessionIcon && iconSize && (
|
||||
<>
|
||||
<SessionIcon iconType={sessionIcon} iconSize={iconSize} theme={props.theme} />
|
||||
<SessionIcon iconType={sessionIcon} iconSize={iconSize} theme={theme} />
|
||||
<div className="spacer-lg" />
|
||||
</>
|
||||
)}
|
||||
|
@ -70,13 +118,16 @@ const SessionConfirmInner = (props: Props) => {
|
|||
</div>
|
||||
|
||||
<div className="session-modal__button-group">
|
||||
<SessionButton text={okText} buttonColor={okTheme} onClick={onClickOk} />
|
||||
{/* <SessionButton text={okText} buttonColor={okTheme} onClick={closeAfterClickOk ? onClickOk : onClickOkWithClose} /> */}
|
||||
<SessionButton text={okText} buttonColor={okTheme} onClick={onClickOkHandler} />
|
||||
|
||||
{!hideCancel && (
|
||||
<SessionButton text={cancelText} buttonColor={closeTheme} onClick={onClickClose} />
|
||||
// <SessionButton text={cancelText} buttonColor={closeTheme} onClick={onClickClose} />
|
||||
<SessionButton text={cancelText} buttonColor={closeTheme} onClick={onClickCancelHandler} />
|
||||
)}
|
||||
</div>
|
||||
</SessionModal>
|
||||
|
||||
</SessionWrapperModal>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -2,21 +2,25 @@ import React, { useState } from 'react';
|
|||
import { ConversationController } from '../../session/conversations/ConversationController';
|
||||
import { SessionModal } from './SessionModal';
|
||||
import { SessionButton } from './SessionButton';
|
||||
import { DefaultTheme, withTheme } from 'styled-components';
|
||||
import { DefaultTheme, withTheme, useTheme } from 'styled-components';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { SessionWrapperModal } from './SessionWrapperModal';
|
||||
|
||||
type Props = {
|
||||
onClickOk: any;
|
||||
onClickClose: any;
|
||||
theme: DefaultTheme;
|
||||
convoId: string;
|
||||
onClickOk?: any;
|
||||
onClickClose?: any;
|
||||
theme?: DefaultTheme;
|
||||
conversationId?: string;
|
||||
};
|
||||
|
||||
const SessionNicknameInner = (props: Props) => {
|
||||
const { onClickOk, onClickClose, convoId, theme } = props;
|
||||
const { onClickOk, onClickClose, conversationId } = props;
|
||||
let { theme } = props;
|
||||
const [nickname, setNickname] = useState('');
|
||||
|
||||
theme = theme ? theme : useTheme();
|
||||
|
||||
/**
|
||||
* Changes the state of nickname variable. If enter is pressed, saves the current
|
||||
* entered nickname value as the nickname.
|
||||
|
@ -34,41 +38,58 @@ const SessionNicknameInner = (props: Props) => {
|
|||
* Saves the currently entered nickname.
|
||||
*/
|
||||
const saveNickname = async () => {
|
||||
if (!convoId) {
|
||||
return;
|
||||
if (!conversationId) {
|
||||
throw "Cant save withou conversation id"
|
||||
// return;
|
||||
}
|
||||
const convo = ConversationController.getInstance().get(convoId);
|
||||
onClickOk(nickname);
|
||||
await convo.setNickname(nickname);
|
||||
const conversation = ConversationController.getInstance().get(conversationId);
|
||||
if (onClickOk) {
|
||||
onClickOk(nickname);
|
||||
}
|
||||
await conversation.setNickname(nickname);
|
||||
onClickClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<SessionModal
|
||||
title={window.i18n('changeNickname')}
|
||||
onClose={onClickClose}
|
||||
showExitIcon={false}
|
||||
showHeader={true}
|
||||
theme={theme}
|
||||
>
|
||||
<div className="session-modal__centered">
|
||||
<span className="subtle">{window.i18n('changeNicknameMessage')}</span>
|
||||
<div className="spacer-lg" />
|
||||
</div>
|
||||
// <SessionModal
|
||||
// title={window.i18n('changeNickname')}
|
||||
// onClose={onClickClose}
|
||||
// showExitIcon={false}
|
||||
// showHeader={true}
|
||||
// theme={theme}
|
||||
// >
|
||||
|
||||
// TODO: Implement showHeader option for modal
|
||||
<SessionWrapperModal
|
||||
title={window.i18n('changeNickname')}
|
||||
onClose={onClickClose}
|
||||
showExitIcon={false}
|
||||
// showHeader={true}
|
||||
theme={theme}
|
||||
>
|
||||
|
||||
<div className="session-modal__centered">
|
||||
<span className="subtle">{window.i18n('changeNicknameMessage')}</span>
|
||||
<div className="spacer-lg" />
|
||||
</div>
|
||||
|
||||
<input
|
||||
autoFocus
|
||||
type="nickname"
|
||||
id="nickname-modal-input"
|
||||
placeholder={window.i18n('nicknamePlaceholder')}
|
||||
onKeyUp={e => {
|
||||
void onNicknameInput(_.cloneDeep(e));
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="session-modal__button-group">
|
||||
<SessionButton text={window.i18n('ok')} onClick={saveNickname} />
|
||||
<SessionButton text={window.i18n('cancel')} onClick={onClickClose} />
|
||||
</div>
|
||||
</SessionWrapperModal>
|
||||
|
||||
<input
|
||||
type="nickname"
|
||||
id="nickname-modal-input"
|
||||
placeholder={window.i18n('nicknamePlaceholder')}
|
||||
onKeyUp={e => {
|
||||
void onNicknameInput(_.cloneDeep(e));
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="session-modal__button-group">
|
||||
<SessionButton text={window.i18n('ok')} onClick={saveNickname} />
|
||||
<SessionButton text={window.i18n('cancel')} onClick={onClickClose} />
|
||||
</div>
|
||||
</SessionModal>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { ToastUtils } from '../../session/utils';
|
|||
import { SessionIconType } from './icon';
|
||||
import { DefaultTheme, withTheme } from 'styled-components';
|
||||
import { getPasswordHash } from '../../data/data';
|
||||
import { SessionWrapperModal } from './SessionWrapperModal';
|
||||
export enum PasswordAction {
|
||||
Set = 'set',
|
||||
Change = 'change',
|
||||
|
@ -72,7 +73,7 @@ class SessionPasswordModalInner extends React.Component<Props, State> {
|
|||
action === PasswordAction.Remove ? SessionButtonColor.Danger : SessionButtonColor.Primary;
|
||||
|
||||
return (
|
||||
<SessionModal
|
||||
<SessionWrapperModal
|
||||
title={window.i18n(`${action}Password`)}
|
||||
onClose={this.closeDialog}
|
||||
theme={this.props.theme}
|
||||
|
@ -119,7 +120,7 @@ class SessionPasswordModalInner extends React.Component<Props, State> {
|
|||
|
||||
<SessionButton text={window.i18n('cancel')} onClick={this.closeDialog} />
|
||||
</div>
|
||||
</SessionModal>
|
||||
</SessionWrapperModal>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { PasswordUtil } from '../../util';
|
|||
import { getPasswordHash } from '../../data/data';
|
||||
import { QRCode } from 'react-qr-svg';
|
||||
import { mn_decode } from '../../session/crypto/mnemonic';
|
||||
import { SessionWrapperModal } from './SessionWrapperModal';
|
||||
|
||||
interface Props {
|
||||
onClose: any;
|
||||
|
@ -62,11 +63,19 @@ class SessionSeedModalInner extends React.Component<Props, State> {
|
|||
return (
|
||||
<>
|
||||
{!loading && (
|
||||
<SessionModal
|
||||
// <SessionModal
|
||||
// title={i18n('showRecoveryPhrase')}
|
||||
// onClose={onClose}
|
||||
// theme={this.props.theme}
|
||||
// >
|
||||
|
||||
<SessionWrapperModal
|
||||
title={i18n('showRecoveryPhrase')}
|
||||
onClose={onClose}
|
||||
theme={this.props.theme}
|
||||
>
|
||||
>
|
||||
|
||||
|
||||
<div className="spacer-sm" />
|
||||
|
||||
{hasPassword && !passwordValid ? (
|
||||
|
@ -74,7 +83,8 @@ class SessionSeedModalInner extends React.Component<Props, State> {
|
|||
) : (
|
||||
<>{this.renderSeedView()}</>
|
||||
)}
|
||||
</SessionModal>
|
||||
</SessionWrapperModal>
|
||||
// </SessionModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { updateConfirmModal } from '../../state/ducks/modalDialog';
|
||||
|
||||
interface Props {
|
||||
active: boolean;
|
||||
|
@ -14,6 +15,8 @@ interface Props {
|
|||
// okTheme: 'danger',
|
||||
// }
|
||||
confirmationDialogParams?: any | undefined;
|
||||
|
||||
updateConfirmModal?: any;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -62,15 +65,25 @@ export class SessionToggle extends React.PureComponent<Props, State> {
|
|||
|
||||
if (
|
||||
this.props.confirmationDialogParams &&
|
||||
this.props.updateConfirmModal &&
|
||||
this.props.confirmationDialogParams.shouldShowConfirm()
|
||||
) {
|
||||
// If item needs a confirmation dialog to turn ON, render it
|
||||
window.confirmationDialog({
|
||||
resolve: () => {
|
||||
const closeConfirmModal = () => {
|
||||
this.props.updateConfirmModal(null);
|
||||
}
|
||||
|
||||
this.props.updateConfirmModal({
|
||||
onClickOk: () => {
|
||||
stateManager(event);
|
||||
closeConfirmModal();
|
||||
},
|
||||
onClickClose: () => {
|
||||
this.props.updateConfirmModal(null);
|
||||
},
|
||||
...this.props.confirmationDialogParams,
|
||||
});
|
||||
updateConfirmModal,
|
||||
})
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
139
ts/components/session/SessionWrapperModal.tsx
Normal file
139
ts/components/session/SessionWrapperModal.tsx
Normal file
|
@ -0,0 +1,139 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon/';
|
||||
import { SessionButton, SessionButtonColor, SessionButtonType } from './SessionButton';
|
||||
import { DefaultTheme } from 'styled-components';
|
||||
|
||||
import { useKeyPress } from 'use-hooks';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
onClose: any;
|
||||
showExitIcon?: boolean;
|
||||
showHeader?: boolean;
|
||||
headerReverse?: boolean;
|
||||
//Maximum of two icons or buttons in header
|
||||
headerIconButtons?: Array<{
|
||||
iconType: SessionIconType;
|
||||
iconRotation: number;
|
||||
onClick?: any;
|
||||
}>;
|
||||
headerButtons?: Array<{
|
||||
buttonType: SessionButtonType;
|
||||
buttonColor: SessionButtonColor;
|
||||
text: string;
|
||||
onClick?: any;
|
||||
}>;
|
||||
theme: DefaultTheme;
|
||||
}
|
||||
|
||||
export type SessionWrapperModalType = {
|
||||
title?: string;
|
||||
showHeader?: boolean;
|
||||
onConfirm?: () => void;
|
||||
onClose?: () => void;
|
||||
showClose?: boolean
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
showExitIcon?: boolean;
|
||||
theme?: any;
|
||||
headerIconButtons?: any[];
|
||||
children: any;
|
||||
headerReverse?: boolean;
|
||||
};
|
||||
|
||||
export const SessionWrapperModal = (props: SessionWrapperModalType) => {
|
||||
const {
|
||||
title,
|
||||
onConfirm,
|
||||
onClose,
|
||||
showHeader = true,
|
||||
showClose = false,
|
||||
confirmText,
|
||||
cancelText,
|
||||
showExitIcon,
|
||||
theme,
|
||||
headerIconButtons,
|
||||
headerReverse
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keyup', keyUpHandler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keyup', keyUpHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const keyUpHandler = ({ key }: any) => {
|
||||
if (key === 'Escape') {
|
||||
if (props.onClose) {
|
||||
props.onClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="loki-dialog modal">
|
||||
<div className="session-confirm-wrapper">
|
||||
<div className="session-modal">
|
||||
|
||||
{showHeader ?
|
||||
<div className={classNames('session-modal__header', headerReverse && 'reverse')}>
|
||||
<div className="session-modal__header__close">
|
||||
{showExitIcon ? (
|
||||
<SessionIconButton
|
||||
iconType={SessionIconType.Exit}
|
||||
iconSize={SessionIconSize.Small}
|
||||
onClick={props.onClose}
|
||||
theme={props.theme}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="session-modal__header__title">{title}</div>
|
||||
<div className="session-modal__header__icons">
|
||||
{headerIconButtons
|
||||
? headerIconButtons.map((iconItem: any) => {
|
||||
return (
|
||||
<SessionIconButton
|
||||
key={iconItem.iconType}
|
||||
iconType={iconItem.iconType}
|
||||
iconSize={SessionIconSize.Large}
|
||||
iconRotation={iconItem.iconRotation}
|
||||
onClick={iconItem.onClick}
|
||||
theme={props.theme}
|
||||
/>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
|
||||
<div className="session-modal__body">
|
||||
<div className="session-modal__centered">
|
||||
{props.children}
|
||||
<div className="session-modal__button-group">
|
||||
|
||||
{onConfirm ? (
|
||||
<SessionButton onClick={props.onConfirm}>
|
||||
{confirmText || window.i18n('ok')}
|
||||
</SessionButton>
|
||||
) : null}
|
||||
|
||||
|
||||
{onClose && showClose ? (
|
||||
<SessionButton onClick={props.onClose}>
|
||||
{cancelText || window.i18n('close')}
|
||||
</SessionButton>
|
||||
) : null}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -6,13 +6,13 @@ import classNames from 'classnames';
|
|||
|
||||
import { SessionCompositionBox, StagedAttachmentType } from './SessionCompositionBox';
|
||||
|
||||
import { Constants } from '../../../session';
|
||||
import { ClosedGroup, Constants, Utils } from '../../../session';
|
||||
import _ from 'lodash';
|
||||
import { AttachmentUtil, GoogleChrome } from '../../../util';
|
||||
import { ConversationHeaderWithDetails } from '../../conversation/ConversationHeader';
|
||||
import { SessionRightPanelWithDetails } from './SessionRightPanel';
|
||||
import { SessionTheme } from '../../../state/ducks/SessionTheme';
|
||||
import { DefaultTheme } from 'styled-components';
|
||||
import { DefaultTheme, useTheme } from 'styled-components';
|
||||
import { SessionMessagesList } from './SessionMessagesList';
|
||||
import { LightboxGallery, MediaItemType } from '../../LightboxGallery';
|
||||
import { Message } from '../../conversation/media-gallery/types/Message';
|
||||
|
@ -38,6 +38,15 @@ import {
|
|||
import { updateMentionsMembers } from '../../../state/ducks/mentionsInput';
|
||||
import { sendDataExtractionNotification } from '../../../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage';
|
||||
|
||||
import { SessionButtonColor } from '../SessionButton';
|
||||
import { AddModeratorsDialog } from '../../conversation/ModeratorsAddDialog';
|
||||
import { RemoveModeratorsDialog } from '../../conversation/ModeratorsRemoveDialog';
|
||||
import { UpdateGroupNameDialog } from '../../conversation/UpdateGroupNameDialog';
|
||||
import { UpdateGroupMembersDialog } from '../../conversation/UpdateGroupMembersDialog';
|
||||
import { getOurNumber } from '../../../state/selectors/user';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { InviteContactsDialog } from '../../conversation/InviteContactsDialog';
|
||||
|
||||
interface State {
|
||||
// Message sending progress
|
||||
messageProgressVisible: boolean;
|
||||
|
@ -69,6 +78,7 @@ interface State {
|
|||
|
||||
// lightbox options
|
||||
lightBoxOptions?: LightBoxOptions;
|
||||
modal: JSX.Element | null;
|
||||
}
|
||||
|
||||
export interface LightBoxOptions {
|
||||
|
@ -109,6 +119,7 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
showOptionsPane: false,
|
||||
stagedAttachments: [],
|
||||
isDraggingFile: false,
|
||||
modal: null,
|
||||
};
|
||||
this.compositionBoxRef = React.createRef();
|
||||
this.messageContainerRef = React.createRef();
|
||||
|
@ -306,6 +317,7 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{this.state.modal ? this.state.modal : null}
|
||||
<div className={classNames('conversation-item__options-pane', showOptionsPane && 'show')}>
|
||||
<SessionRightPanelWithDetails {...this.getRightPanelProps()} />
|
||||
</div>
|
||||
|
@ -390,12 +402,10 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
onSetNotificationForConvo: conversation.setNotificationOption,
|
||||
onDeleteMessages: conversation.deleteMessages,
|
||||
onDeleteSelectedMessages: this.deleteSelectedMessages,
|
||||
onChangeNickname: conversation.changeNickname,
|
||||
onClearNickname: conversation.clearNickname,
|
||||
onCloseOverlay: () => {
|
||||
this.setState({ selectedMessages: [] });
|
||||
},
|
||||
onDeleteContact: conversation.deleteContact,
|
||||
|
||||
onGoBack: () => {
|
||||
this.setState({
|
||||
|
@ -470,9 +480,23 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
onDownloadAttachment: this.saveAttachment,
|
||||
messageContainerRef: this.messageContainerRef,
|
||||
onDeleteSelectedMessages: this.deleteSelectedMessages,
|
||||
updateSessionConversationModal: this.updateSessionConversationModal,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting this to a JSX element that will be rendered if non-null.
|
||||
* @param update Value to set the modal state to
|
||||
*/
|
||||
private updateSessionConversationModal(update: JSX.Element | null) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
modal: update,
|
||||
});
|
||||
}
|
||||
// tslint:disable: member-ordering
|
||||
|
||||
// tslint:disable-next-line: max-func-body-length
|
||||
public getRightPanelProps() {
|
||||
const { selectedConversationKey } = this.props;
|
||||
const conversation = ConversationController.getInstance().getOrThrow(selectedConversationKey);
|
||||
|
@ -517,9 +541,61 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
},
|
||||
|
||||
onUpdateGroupName: () => {
|
||||
window.Whisper.events.trigger('updateGroupName', conversation);
|
||||
// warrick: remove trigger once everything is cleaned up
|
||||
// window.Whisper.events.trigger('updateGroupName', conversation);
|
||||
const avatarPath = conversation.getAvatarPath();
|
||||
const groupName = conversation.getName();
|
||||
const groupId = conversation.id;
|
||||
const members = conversation.get('members') || [];
|
||||
const isPublic = conversation.isPublic();
|
||||
|
||||
let isAdmin = true;
|
||||
let titleText;
|
||||
|
||||
titleText = window.i18n('updateGroupDialogTitle', groupName);
|
||||
|
||||
if (isPublic) {
|
||||
// fix the title
|
||||
// I'd much prefer to integrate mods with groupAdmins
|
||||
// but lets discuss first...
|
||||
isAdmin = conversation.isAdmin(window.storage.get('primaryDevicePubKey'));
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
this.setState({ ...this.state, modal: null });
|
||||
};
|
||||
|
||||
const onUpdateGroupNameSubmit = (newGroupName: string, newAvatarPath: string) => {
|
||||
if (newGroupName !== groupName || newAvatarPath !== avatarPath) {
|
||||
void ClosedGroup.initiateGroupUpdate(groupId, newGroupName, members, newAvatarPath);
|
||||
}
|
||||
};
|
||||
|
||||
this.setState({
|
||||
...this.state,
|
||||
modal: (
|
||||
<UpdateGroupNameDialog
|
||||
titleText={titleText}
|
||||
pubkey={conversation.id}
|
||||
isPublic={conversation.isPublic()}
|
||||
groupName={groupName}
|
||||
okText={window.i18n('ok')}
|
||||
cancelText={window.i18n('cancel')}
|
||||
isAdmin={isAdmin}
|
||||
i18n={window.i18n}
|
||||
onSubmit={onUpdateGroupNameSubmit}
|
||||
onClose={onClose}
|
||||
// avatar stuff
|
||||
avatarPath={avatarPath || ''}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
),
|
||||
});
|
||||
},
|
||||
onUpdateGroupMembers: async () => {
|
||||
// window.Whisper.events.trigger('updateGroupMembers', conversation);
|
||||
// return;
|
||||
|
||||
if (conversation.isMediumGroup()) {
|
||||
// make sure all the members' convo exists so we can add or remove them
|
||||
await Promise.all(
|
||||
|
@ -533,21 +609,161 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
)
|
||||
);
|
||||
}
|
||||
window.Whisper.events.trigger('updateGroupMembers', conversation);
|
||||
|
||||
const groupName = conversation.getName();
|
||||
const isPublic = conversation.isPublic();
|
||||
const groupId = conversation.id;
|
||||
const members = conversation.get('members') || [];
|
||||
const avatarPath = conversation.getAvatarPath();
|
||||
const theme = this.props.theme;
|
||||
|
||||
const titleText = window.i18n('updateGroupDialogTitle', groupName);
|
||||
|
||||
const ourPK = Utils.UserUtils.getOurPubKeyStrFromCache();
|
||||
|
||||
let admins = conversation.get('groupAdmins');
|
||||
const isAdmin = conversation.get('groupAdmins')?.includes(ourPK) ? true : false;
|
||||
|
||||
const convos = ConversationController.getInstance()
|
||||
.getConversations()
|
||||
.filter(d => !!d);
|
||||
|
||||
let existingMembers = conversation.get('members') || [];
|
||||
let existingZombies = conversation.get('zombies') || [];
|
||||
|
||||
let contactsAndMembers = convos.filter(
|
||||
d => existingMembers.includes(d.id) && d.isPrivate() && !d.isMe()
|
||||
);
|
||||
|
||||
// contactsAndMembers = _.uniqBy(contactsAndMembers, true, d => d.id);
|
||||
contactsAndMembers = _.uniqBy(contactsAndMembers, 'id');
|
||||
|
||||
// at least make sure it's an array
|
||||
if (!Array.isArray(existingMembers)) {
|
||||
existingMembers = [];
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
modal: null,
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async (newMembers: Array<string>) => {
|
||||
const _ = window.Lodash;
|
||||
const ourPK = Utils.UserUtils.getOurPubKeyStrFromCache();
|
||||
|
||||
const allMembersAfterUpdate = window.Lodash.concat(newMembers, [ourPK]);
|
||||
|
||||
if (!isAdmin) {
|
||||
window.log.warn('Skipping update of members, we are not the admin');
|
||||
return;
|
||||
}
|
||||
// new members won't include the zombies. We are the admin and we want to remove them not matter what
|
||||
|
||||
// We need to NOT trigger an group update if the list of member is the same.
|
||||
// we need to merge all members, including zombies for this call.
|
||||
|
||||
// we consider that the admin ALWAYS wants to remove zombies (actually they should be removed
|
||||
// automatically by him when the LEFT message is received)
|
||||
const allExistingMembersWithZombies = _.uniq(existingMembers.concat(existingZombies));
|
||||
|
||||
const notPresentInOld = allMembersAfterUpdate.filter(
|
||||
(m: string) => !allExistingMembersWithZombies.includes(m)
|
||||
);
|
||||
|
||||
// be sure to include zombies in here
|
||||
const membersToRemove = allExistingMembersWithZombies.filter(
|
||||
(m: string) => !allMembersAfterUpdate.includes(m)
|
||||
);
|
||||
|
||||
const xor = _.xor(membersToRemove, notPresentInOld);
|
||||
if (xor.length === 0) {
|
||||
window.log.info('skipping group update: no detected changes in group member list');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If any extra devices of removed exist in newMembers, ensure that you filter them
|
||||
// Note: I think this is useless
|
||||
const filteredMembers = allMembersAfterUpdate.filter(
|
||||
(member: string) => !_.includes(membersToRemove, member)
|
||||
);
|
||||
|
||||
void ClosedGroup.initiateGroupUpdate(groupId, groupName, filteredMembers, avatarPath);
|
||||
};
|
||||
|
||||
this.setState({
|
||||
...this.state,
|
||||
modal: (
|
||||
// tslint:disable-next-line: use-simple-attributes
|
||||
<UpdateGroupMembersDialog
|
||||
titleText={titleText}
|
||||
isPublic={isPublic}
|
||||
admins={admins || []}
|
||||
onSubmit={onSubmit}
|
||||
onClose={onClose}
|
||||
okText={window.i18n('ok')}
|
||||
cancelText={window.i18n('cancel')}
|
||||
contactList={contactsAndMembers}
|
||||
isAdmin={isAdmin}
|
||||
existingMembers={existingMembers}
|
||||
existingZombies={existingZombies}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
// warrick: delete old code
|
||||
},
|
||||
onInviteContacts: () => {
|
||||
window.Whisper.events.trigger('inviteContacts', conversation);
|
||||
this.setState({
|
||||
...this.state,
|
||||
modal: (
|
||||
<InviteContactsDialog
|
||||
convo={conversation}
|
||||
onClose={() => {
|
||||
this.setState({ ...this.state, modal: null });
|
||||
}}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
),
|
||||
});
|
||||
},
|
||||
onDeleteContact: conversation.deleteContact,
|
||||
onLeaveGroup: () => {
|
||||
window.Whisper.events.trigger('leaveClosedGroup', conversation);
|
||||
},
|
||||
onAddModerators: () => {
|
||||
window.Whisper.events.trigger('addModerators', conversation);
|
||||
// window.Whisper.events.trigger('addModerators', conversation);
|
||||
this.setState({
|
||||
...this.state,
|
||||
modal: (
|
||||
<AddModeratorsDialog
|
||||
convo={conversation}
|
||||
onClose={() => {
|
||||
this.setState({ ...this.state, modal: null });
|
||||
}}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
onRemoveModerators: () => {
|
||||
window.Whisper.events.trigger('removeModerators', conversation);
|
||||
// window.Whisper.events.trigger('removeModerators', conversation);
|
||||
this.setState({
|
||||
...this.state,
|
||||
modal: (
|
||||
<RemoveModeratorsDialog
|
||||
convo={conversation}
|
||||
onClose={() => {
|
||||
this.setState({ ...this.state, modal: null });
|
||||
}}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
),
|
||||
});
|
||||
},
|
||||
onShowLightBox: (lightBoxOptions?: LightBoxOptions) => {
|
||||
this.setState({ lightBoxOptions });
|
||||
|
@ -692,13 +908,20 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
const okText = window.i18n(isServerDeletable ? 'deleteForEveryone' : 'delete');
|
||||
|
||||
if (askUserForConfirmation) {
|
||||
window.confirmationDialog({
|
||||
const onClickClose = () => {
|
||||
this.props.actions.updateConfirmModal(null);
|
||||
};
|
||||
|
||||
this.props.actions.updateConfirmModal({
|
||||
title,
|
||||
message: warningMessage,
|
||||
okText,
|
||||
okTheme: 'danger',
|
||||
resolve: doDelete,
|
||||
okTheme: SessionButtonColor.Danger,
|
||||
onClickOk: doDelete,
|
||||
onClickClose,
|
||||
closeAfterClick: true,
|
||||
});
|
||||
} else {
|
||||
void doDelete();
|
||||
|
|
|
@ -55,6 +55,7 @@ interface Props {
|
|||
messageSender: string;
|
||||
}) => void;
|
||||
onDeleteSelectedMessages: () => Promise<void>;
|
||||
updateSessionConversationModal: (modal: JSX.Element | null) => any;
|
||||
}
|
||||
|
||||
export class SessionMessagesList extends React.Component<Props, State> {
|
||||
|
@ -345,6 +346,8 @@ export class SessionMessagesList extends React.Component<Props, State> {
|
|||
};
|
||||
}
|
||||
|
||||
messageProps.updateSessionConversationModal = this.props.updateSessionConversationModal
|
||||
|
||||
return <Message {...messageProps} key={messageProps.id} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -246,10 +246,10 @@ class SessionRightPanel extends React.Component<Props, State> {
|
|||
const leaveGroupString = isPublic
|
||||
? window.i18n('leaveGroup')
|
||||
: isKickedFromGroup
|
||||
? window.i18n('youGotKickedFromGroup')
|
||||
: left
|
||||
? window.i18n('youLeftTheGroup')
|
||||
: window.i18n('leaveGroup');
|
||||
? window.i18n('youGotKickedFromGroup')
|
||||
: left
|
||||
? window.i18n('youLeftTheGroup')
|
||||
: window.i18n('leaveGroup');
|
||||
|
||||
const disappearingMessagesOptions = timerOptions.map(option => {
|
||||
return {
|
||||
|
|
|
@ -5,6 +5,7 @@ export enum SessionIconType {
|
|||
ChatBubble = 'chatBubble',
|
||||
Check = 'check',
|
||||
Chevron = 'chevron',
|
||||
Circle = 'circle',
|
||||
CircleCheck = 'circleCheck',
|
||||
DoubleCheckCircleFilled = 'doubleCheckCircleFilled',
|
||||
CirclePlus = 'circlePlus',
|
||||
|
@ -99,6 +100,11 @@ export const icons = {
|
|||
viewBox: '1.5 5.5 21 12',
|
||||
ratio: 1,
|
||||
},
|
||||
[SessionIconType.Circle]: {
|
||||
path: 'M 100, 100m -75, 0a 75,75 0 1,0 150,0a 75,75 0 1,0 -150,0',
|
||||
viewBox: '0 0 200 200',
|
||||
ratio: 1,
|
||||
},
|
||||
[SessionIconType.CircleCheck]: {
|
||||
path:
|
||||
'M4.77,7.61c-0.15-0.15-0.38-0.15-0.53,0c-0.15,0.15-0.15,0.38,0,0.53l1.88,1.88c0.15,0.15,0.38,0.15,0.53,0 l4.13-4.12c0.15-0.15,0.15-0.38,0-0.53c-0.15-0.15-0.38-0.15-0.53,0L6.38,9.22L4.77,7.61z',
|
||||
|
|
|
@ -9,6 +9,9 @@ export type SessionIconProps = {
|
|||
iconColor?: string;
|
||||
iconRotation?: number;
|
||||
rotateDuration?: number;
|
||||
glowDuration?: number;
|
||||
borderRadius?: number;
|
||||
glowStartDelay?: number;
|
||||
theme: DefaultTheme;
|
||||
};
|
||||
|
||||
|
@ -35,6 +38,17 @@ const getIconDimensionFromIconSize = (iconSize: SessionIconSize | number) => {
|
|||
}
|
||||
};
|
||||
|
||||
type StyledSvgProps = {
|
||||
width: string | number;
|
||||
height: string | number;
|
||||
iconRotation: number;
|
||||
rotateDuration?: number;
|
||||
borderRadius?: number;
|
||||
glowDuration?: number;
|
||||
glowStartDelay?: number;
|
||||
iconColor?: string;
|
||||
};
|
||||
|
||||
const rotate = keyframes`
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
|
@ -44,27 +58,57 @@ const rotate = keyframes`
|
|||
}
|
||||
`;
|
||||
|
||||
const animation = (props: { rotateDuration?: any }) => {
|
||||
/**
|
||||
* Creates a glow animation made for multiple element sequentially
|
||||
* @param color
|
||||
* @param glowDuration
|
||||
* @param glowStartDelay
|
||||
* @returns
|
||||
*/
|
||||
const glow = (color: string, glowDuration: number, glowStartDelay: number) => {
|
||||
let dropShadowType = `drop-shadow(0px 0px 6px ${color}) `;
|
||||
//increase shadow intensity by 3
|
||||
let dropShadow = `${dropShadowType.repeat(2)};`;
|
||||
|
||||
// TODO: Decrease dropshadow for last frame
|
||||
// creating keyframe for sequential animations
|
||||
let kf = '';
|
||||
for (let i = 0; i <= glowDuration; i++) {
|
||||
// const percent = (100 / glowDuration) * i;
|
||||
const percent = (100 / glowDuration) * i;
|
||||
if (i === glowStartDelay) {
|
||||
kf += `${percent}% {
|
||||
filter: ${dropShadow}
|
||||
}`;
|
||||
} else {
|
||||
kf += `${percent}% {
|
||||
filter: none;
|
||||
}`;
|
||||
}
|
||||
}
|
||||
return keyframes`${kf}`;
|
||||
};
|
||||
|
||||
const animation = (props: any) => {
|
||||
if (props.rotateDuration) {
|
||||
return css`
|
||||
${rotate} ${props.rotateDuration}s infinite linear;
|
||||
`;
|
||||
} else if (props.glowDuration !== undefined && props.glowStartDelay !== undefined) {
|
||||
return css`
|
||||
${glow(props.iconColor, props.glowDuration, props.glowStartDelay)} ${2}s ease-in infinite;
|
||||
`;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
type StyledSvgProps = {
|
||||
width: string | number;
|
||||
iconRotation: number;
|
||||
rotateDuration?: number;
|
||||
};
|
||||
|
||||
//tslint:disable no-unnecessary-callback-wrapper
|
||||
const Svg = styled.svg<StyledSvgProps>`
|
||||
width: ${props => props.width};
|
||||
transform: ${props => `rotate(${props.iconRotation}deg)`};
|
||||
animation: ${props => animation(props)};
|
||||
border-radius: ${props => props.borderRadius};
|
||||
`;
|
||||
//tslint:enable no-unnecessary-callback-wrapper
|
||||
|
||||
|
@ -76,6 +120,9 @@ const SessionSvg = (props: {
|
|||
iconRotation: number;
|
||||
iconColor?: string;
|
||||
rotateDuration?: number;
|
||||
glowDuration?: number;
|
||||
glowStartDelay?: number;
|
||||
borderRadius?: number;
|
||||
theme: DefaultTheme;
|
||||
}) => {
|
||||
const colorSvg = props.iconColor || props?.theme?.colors.textColor;
|
||||
|
@ -90,6 +137,15 @@ const SessionSvg = (props: {
|
|||
|
||||
return (
|
||||
<Svg {...propsToPick}>
|
||||
{/* { props.glowDuration ?
|
||||
<defs>
|
||||
<filter>
|
||||
<feDropShadow dx="0.2" dy="0.4" stdDeviation="0.2" />
|
||||
</filter>
|
||||
</defs>
|
||||
:
|
||||
null
|
||||
} */}
|
||||
{pathArray.map((path, index) => {
|
||||
return <path key={index} fill={colorSvg} d={path} />;
|
||||
})}
|
||||
|
@ -98,7 +154,15 @@ const SessionSvg = (props: {
|
|||
};
|
||||
|
||||
export const SessionIcon = (props: SessionIconProps) => {
|
||||
const { iconType, iconColor, theme, rotateDuration } = props;
|
||||
const {
|
||||
iconType,
|
||||
iconColor,
|
||||
theme,
|
||||
rotateDuration,
|
||||
glowDuration,
|
||||
borderRadius,
|
||||
glowStartDelay,
|
||||
} = props;
|
||||
let { iconSize, iconRotation } = props;
|
||||
iconSize = iconSize || SessionIconSize.Medium;
|
||||
iconRotation = iconRotation || 0;
|
||||
|
@ -117,6 +181,9 @@ export const SessionIcon = (props: SessionIconProps) => {
|
|||
width={iconDimensions * ratio}
|
||||
height={iconDimensions}
|
||||
rotateDuration={rotateDuration}
|
||||
glowDuration={glowDuration}
|
||||
glowStartDelay={glowStartDelay}
|
||||
borderRadius={borderRadius}
|
||||
iconRotation={iconRotation}
|
||||
iconColor={iconColor}
|
||||
theme={theme}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { animation, Menu } from 'react-contexify';
|
||||
import {
|
||||
getAddModeratorsMenuItem,
|
||||
|
@ -20,6 +20,7 @@ import { NotificationForConvoOption, TimerOption } from '../../conversation/Conv
|
|||
import { ConversationNotificationSettingType } from '../../../models/conversation';
|
||||
|
||||
export type PropsConversationHeaderMenu = {
|
||||
id: string;
|
||||
triggerId: string;
|
||||
isMe: boolean;
|
||||
isPublic?: boolean;
|
||||
|
@ -32,6 +33,7 @@ export type PropsConversationHeaderMenu = {
|
|||
currentNotificationSetting: ConversationNotificationSettingType;
|
||||
isPrivate: boolean;
|
||||
isBlocked: boolean;
|
||||
theme: any;
|
||||
hasNickname?: boolean;
|
||||
|
||||
onDeleteMessages?: () => void;
|
||||
|
@ -54,6 +56,7 @@ export type PropsConversationHeaderMenu = {
|
|||
|
||||
export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
|
||||
const {
|
||||
id,
|
||||
triggerId,
|
||||
isMe,
|
||||
isPublic,
|
||||
|
@ -85,38 +88,60 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
|
|||
onSetNotificationForConvo,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Menu id={triggerId} animation={animation.fade}>
|
||||
{getDisappearingMenuItem(
|
||||
isPublic,
|
||||
isKickedFromGroup,
|
||||
left,
|
||||
isBlocked,
|
||||
timerOptions,
|
||||
onSetDisappearingMessages
|
||||
)}
|
||||
{getNotificationForConvoMenuItem(
|
||||
isKickedFromGroup,
|
||||
left,
|
||||
isBlocked,
|
||||
notificationForConvo,
|
||||
currentNotificationSetting,
|
||||
onSetNotificationForConvo
|
||||
)}
|
||||
{getBlockMenuItem(isMe, isPrivate, isBlocked, onBlockUser, onUnblockUser)}
|
||||
const [modal, setModal] = useState<any>(null);
|
||||
|
||||
{getCopyMenuItem(isPublic, isGroup, onCopyPublicKey)}
|
||||
{getMarkAllReadMenuItem(onMarkAllRead)}
|
||||
{getChangeNicknameMenuItem(isMe, onChangeNickname, isGroup)}
|
||||
{getClearNicknameMenuItem(isMe, hasNickname, onClearNickname, isGroup)}
|
||||
{getDeleteMessagesMenuItem(isPublic, onDeleteMessages)}
|
||||
{getAddModeratorsMenuItem(isAdmin, isKickedFromGroup, onAddModerators)}
|
||||
{getRemoveModeratorsMenuItem(isAdmin, isKickedFromGroup, onRemoveModerators)}
|
||||
{getUpdateGroupNameMenuItem(isAdmin, isKickedFromGroup, left, onUpdateGroupName)}
|
||||
{getLeaveGroupMenuItem(isKickedFromGroup, left, isGroup, isPublic, onLeaveGroup)}
|
||||
{/* TODO: add delete group */}
|
||||
{getInviteContactMenuItem(isGroup, isPublic, onInviteContacts)}
|
||||
{getDeleteContactMenuItem(isMe, isGroup, isPublic, left, isKickedFromGroup, onDeleteContact)}
|
||||
</Menu>
|
||||
return (
|
||||
<>
|
||||
{modal ? modal : null}
|
||||
|
||||
<Menu id={triggerId} animation={animation.fade}>
|
||||
{getDisappearingMenuItem(
|
||||
isPublic,
|
||||
isKickedFromGroup,
|
||||
left,
|
||||
isBlocked,
|
||||
timerOptions,
|
||||
onSetDisappearingMessages
|
||||
)}
|
||||
{getNotificationForConvoMenuItem(
|
||||
isKickedFromGroup,
|
||||
left,
|
||||
isBlocked,
|
||||
notificationForConvo,
|
||||
currentNotificationSetting,
|
||||
onSetNotificationForConvo
|
||||
)}
|
||||
{getBlockMenuItem(isMe, isPrivate, isBlocked, onBlockUser, onUnblockUser)}
|
||||
|
||||
{getCopyMenuItem(isPublic, isGroup, onCopyPublicKey)}
|
||||
{getMarkAllReadMenuItem(onMarkAllRead)}
|
||||
{getChangeNicknameMenuItem(isMe, onChangeNickname, isGroup, id, setModal)}
|
||||
{getClearNicknameMenuItem(isMe, hasNickname, onClearNickname, isGroup)}
|
||||
{getDeleteMessagesMenuItem(isPublic, onDeleteMessages, id)}
|
||||
{getAddModeratorsMenuItem(isAdmin, isKickedFromGroup, onAddModerators)}
|
||||
{getRemoveModeratorsMenuItem(isAdmin, isKickedFromGroup, onRemoveModerators)}
|
||||
{getUpdateGroupNameMenuItem(isAdmin, isKickedFromGroup, left, onUpdateGroupName)}
|
||||
{getLeaveGroupMenuItem(
|
||||
isKickedFromGroup,
|
||||
left,
|
||||
isGroup,
|
||||
isPublic,
|
||||
onLeaveGroup,
|
||||
id,
|
||||
setModal
|
||||
)}
|
||||
{/* TODO: add delete group */}
|
||||
{getInviteContactMenuItem(isGroup, isPublic, onInviteContacts)}
|
||||
{getDeleteContactMenuItem(
|
||||
isMe,
|
||||
isGroup,
|
||||
isPublic,
|
||||
left,
|
||||
isKickedFromGroup,
|
||||
onDeleteContact,
|
||||
id
|
||||
)}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { animation, Menu } from 'react-contexify';
|
||||
import { ConversationTypeEnum } from '../../../models/conversation';
|
||||
|
||||
|
@ -15,6 +15,7 @@ import {
|
|||
} from './Menu';
|
||||
|
||||
export type PropsContextConversationItem = {
|
||||
id: string;
|
||||
triggerId: string;
|
||||
type: ConversationTypeEnum;
|
||||
isMe: boolean;
|
||||
|
@ -23,6 +24,7 @@ export type PropsContextConversationItem = {
|
|||
hasNickname?: boolean;
|
||||
isKickedFromGroup?: boolean;
|
||||
left?: boolean;
|
||||
theme?: any;
|
||||
|
||||
onDeleteMessages?: () => void;
|
||||
onDeleteContact?: () => void;
|
||||
|
@ -38,6 +40,7 @@ export type PropsContextConversationItem = {
|
|||
|
||||
export const ConversationListItemContextMenu = (props: PropsContextConversationItem) => {
|
||||
const {
|
||||
id,
|
||||
triggerId,
|
||||
isBlocked,
|
||||
isMe,
|
||||
|
@ -56,28 +59,51 @@ export const ConversationListItemContextMenu = (props: PropsContextConversationI
|
|||
onInviteContacts,
|
||||
onLeaveGroup,
|
||||
onChangeNickname,
|
||||
theme,
|
||||
} = props;
|
||||
|
||||
const isGroup = type === 'group';
|
||||
|
||||
return (
|
||||
<Menu id={triggerId} animation={animation.fade}>
|
||||
{getBlockMenuItem(
|
||||
isMe,
|
||||
type === ConversationTypeEnum.PRIVATE,
|
||||
isBlocked,
|
||||
onBlockContact,
|
||||
onUnblockContact
|
||||
)}
|
||||
{getCopyMenuItem(isPublic, isGroup, onCopyPublicKey)}
|
||||
{getMarkAllReadMenuItem(onMarkAllRead)}
|
||||
{getChangeNicknameMenuItem(isMe, onChangeNickname, isGroup)}
|
||||
{getClearNicknameMenuItem(isMe, hasNickname, onClearNickname, isGroup)}
|
||||
const [modal, setModal] = useState<any>(null);
|
||||
|
||||
{getDeleteMessagesMenuItem(isPublic, onDeleteMessages)}
|
||||
{getInviteContactMenuItem(isGroup, isPublic, onInviteContacts)}
|
||||
{getDeleteContactMenuItem(isMe, isGroup, isPublic, left, isKickedFromGroup, onDeleteContact)}
|
||||
{getLeaveGroupMenuItem(isKickedFromGroup, left, isGroup, isPublic, onLeaveGroup)}
|
||||
</Menu>
|
||||
return (
|
||||
<>
|
||||
{modal ? modal : null}
|
||||
|
||||
<Menu id={triggerId} animation={animation.fade}>
|
||||
{getBlockMenuItem(
|
||||
isMe,
|
||||
type === ConversationTypeEnum.PRIVATE,
|
||||
isBlocked,
|
||||
onBlockContact,
|
||||
onUnblockContact
|
||||
)}
|
||||
{getCopyMenuItem(isPublic, isGroup, onCopyPublicKey)}
|
||||
{getMarkAllReadMenuItem(onMarkAllRead)}
|
||||
{getChangeNicknameMenuItem(isMe, onChangeNickname, isGroup, id, setModal)}
|
||||
{getClearNicknameMenuItem(isMe, hasNickname, onClearNickname, isGroup)}
|
||||
|
||||
{getDeleteMessagesMenuItem(isPublic, onDeleteMessages, id)}
|
||||
{getInviteContactMenuItem(isGroup, isPublic, onInviteContacts)}
|
||||
{getDeleteContactMenuItem(
|
||||
isMe,
|
||||
isGroup,
|
||||
isPublic,
|
||||
left,
|
||||
isKickedFromGroup,
|
||||
onDeleteContact,
|
||||
id
|
||||
)}
|
||||
{getLeaveGroupMenuItem(
|
||||
isKickedFromGroup,
|
||||
left,
|
||||
isGroup,
|
||||
isPublic,
|
||||
onLeaveGroup,
|
||||
id,
|
||||
setModal
|
||||
)}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
import React from 'react';
|
||||
|
||||
import { NotificationForConvoOption, TimerOption } from '../../conversation/ConversationHeader';
|
||||
import { Item, Submenu } from 'react-contexify';
|
||||
import { ConversationNotificationSettingType } from '../../../models/conversation';
|
||||
import { SessionNicknameDialog } from '../SessionNicknameDialog';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { updateConfirmModal } from '../../../state/ducks/modalDialog';
|
||||
import { ConversationController } from '../../../session/conversations';
|
||||
import { UserUtils } from '../../../session/utils';
|
||||
import { AdminLeaveClosedGroupDialog } from '../../conversation/AdminLeaveClosedGroupDialog';
|
||||
import { useTheme } from 'styled-components';
|
||||
|
||||
function showTimerOptions(
|
||||
isPublic: boolean,
|
||||
|
@ -100,7 +108,8 @@ export function getDeleteContactMenuItem(
|
|||
isPublic: boolean | undefined,
|
||||
isLeft: boolean | undefined,
|
||||
isKickedFromGroup: boolean | undefined,
|
||||
action: any
|
||||
action: any,
|
||||
id: string
|
||||
): JSX.Element | null {
|
||||
if (
|
||||
showDeleteContact(
|
||||
|
@ -111,10 +120,35 @@ export function getDeleteContactMenuItem(
|
|||
Boolean(isKickedFromGroup)
|
||||
)
|
||||
) {
|
||||
let menuItemText: string;
|
||||
if (isPublic) {
|
||||
return <Item onClick={action}>{window.i18n('leaveGroup')}</Item>;
|
||||
menuItemText = window.i18n('leaveGroup');
|
||||
} else {
|
||||
menuItemText = window.i18n('delete');
|
||||
}
|
||||
return <Item onClick={action}>{window.i18n('delete')}</Item>;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const onClickClose = () => {
|
||||
dispatch(updateConfirmModal(null));
|
||||
};
|
||||
|
||||
const showConfirmationModal = () => {
|
||||
dispatch(
|
||||
updateConfirmModal({
|
||||
title: menuItemText,
|
||||
message: isGroup
|
||||
? window.i18n('leaveGroupConfirmation')
|
||||
: window.i18n('deleteContactConfirmation'),
|
||||
onClickClose,
|
||||
onClickOk: () => {
|
||||
void ConversationController.getInstance().deleteContact(id);
|
||||
onClickClose();
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return <Item onClick={showConfirmationModal}>{menuItemText}</Item>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -124,12 +158,60 @@ export function getLeaveGroupMenuItem(
|
|||
left: boolean | undefined,
|
||||
isGroup: boolean | undefined,
|
||||
isPublic: boolean | undefined,
|
||||
action: any
|
||||
action: any,
|
||||
id: string,
|
||||
setModal: any
|
||||
): JSX.Element | null {
|
||||
if (
|
||||
showLeaveGroup(Boolean(isKickedFromGroup), Boolean(left), Boolean(isGroup), Boolean(isPublic))
|
||||
) {
|
||||
return <Item onClick={action}>{window.i18n('leaveGroup')}</Item>;
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const conversation = ConversationController.getInstance().get(id);
|
||||
|
||||
const onClickClose = () => {
|
||||
dispatch(updateConfirmModal(null));
|
||||
};
|
||||
|
||||
const openConfirmationModal = () => {
|
||||
if (!conversation.isGroup()) {
|
||||
throw new Error('showLeaveGroupDialog() called with a non group convo.');
|
||||
}
|
||||
|
||||
const title = window.i18n('leaveGroup');
|
||||
const message = window.i18n('leaveGroupConfirmation');
|
||||
const ourPK = UserUtils.getOurPubKeyStrFromCache();
|
||||
const isAdmin = (conversation.get('groupAdmins') || []).includes(ourPK);
|
||||
const isClosedGroup = conversation.get('is_medium_group') || false;
|
||||
|
||||
// if this is not a closed group, or we are not admin, we can just show a confirmation dialog
|
||||
if (!isClosedGroup || (isClosedGroup && !isAdmin)) {
|
||||
dispatch(
|
||||
updateConfirmModal({
|
||||
title,
|
||||
message,
|
||||
onClickOk: () => {
|
||||
void conversation.leaveClosedGroup();
|
||||
onClickClose();
|
||||
},
|
||||
onClickClose,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setModal(
|
||||
<AdminLeaveClosedGroupDialog
|
||||
groupName={conversation.getName()}
|
||||
onSubmit={conversation.leaveClosedGroup}
|
||||
onClose={() => {
|
||||
setModal(null);
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return <Item onClick={openConfirmationModal}>{window.i18n('leaveGroup')}</Item>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -302,20 +384,58 @@ export function getClearNicknameMenuItem(
|
|||
export function getChangeNicknameMenuItem(
|
||||
isMe: boolean | undefined,
|
||||
action: any,
|
||||
isGroup: boolean | undefined
|
||||
isGroup: boolean | undefined,
|
||||
conversationId?: string,
|
||||
setModal?: any
|
||||
): JSX.Element | null {
|
||||
if (showChangeNickname(Boolean(isMe), Boolean(isGroup))) {
|
||||
return <Item onClick={action}>{window.i18n('changeNickname')}</Item>;
|
||||
const clearModal = () => {
|
||||
setModal(null);
|
||||
};
|
||||
|
||||
const onClickCustom = () => {
|
||||
setModal(<SessionNicknameDialog onClickClose={clearModal} conversationId={conversationId} />);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Item onClick={onClickCustom}>{window.i18n('changeNickname')}</Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getDeleteMessagesMenuItem(
|
||||
isPublic: boolean | undefined,
|
||||
action: any
|
||||
action: any,
|
||||
id: string
|
||||
): JSX.Element | null {
|
||||
if (showDeleteMessages(Boolean(isPublic))) {
|
||||
return <Item onClick={action}>{window.i18n('deleteMessages')}</Item>;
|
||||
const dispatch = useDispatch();
|
||||
const conversation = ConversationController.getInstance().get(id);
|
||||
|
||||
const onClickClose = () => {
|
||||
dispatch(updateConfirmModal(null));
|
||||
};
|
||||
|
||||
const onClickOk = () => {
|
||||
void conversation.destroyMessages();
|
||||
onClickClose();
|
||||
};
|
||||
|
||||
const openConfirmationModal = () => {
|
||||
dispatch(
|
||||
updateConfirmModal({
|
||||
title: window.i18n('deleteMessages'),
|
||||
message: window.i18n('deleteConversationConfirmation'),
|
||||
onClickOk,
|
||||
onClickClose,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return <Item onClick={openConfirmationModal}>{window.i18n('deleteMessages')}</Item>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { SessionToggle } from '../SessionToggle';
|
|||
import { SessionButton } from '../SessionButton';
|
||||
import { SessionSettingType } from './SessionSettings';
|
||||
import { SessionRadioGroup } from '../SessionRadioGroup';
|
||||
import { SessionConfirmDialogProps } from '../SessionConfirm';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
|
@ -17,7 +18,10 @@ interface Props {
|
|||
onClick?: any;
|
||||
onSliderChange?: any;
|
||||
content: any;
|
||||
confirmationDialogParams?: any;
|
||||
confirmationDialogParams?: SessionConfirmDialogProps;
|
||||
|
||||
// for updating modal in redux
|
||||
updateConfirmModal?: any
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -61,6 +65,7 @@ export class SessionSettingListItem extends React.Component<Props, State> {
|
|||
active={Boolean(value)}
|
||||
onClick={this.handleClick}
|
||||
confirmationDialogParams={this.props.confirmationDialogParams}
|
||||
updateConfirmModal={this.props.updateConfirmModal}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -13,6 +13,9 @@ import { connect } from 'react-redux';
|
|||
import { getPasswordHash } from '../../../../ts/data/data';
|
||||
import { SpacerLG } from '../../basic/Text';
|
||||
import { shell } from 'electron';
|
||||
import { PasswordAction, SessionPasswordModal } from '../SessionPasswordModal';
|
||||
import { SessionConfirmDialogProps } from '../SessionConfirm';
|
||||
import { mapDispatchToProps } from '../../../state/actions';
|
||||
|
||||
export enum SessionSettingCategory {
|
||||
Appearance = 'appearance',
|
||||
|
@ -35,6 +38,7 @@ export interface SettingsViewProps {
|
|||
// pass the conversation as props, so our render is called everytime they change.
|
||||
// we have to do this to make the list refresh on unblock()
|
||||
conversations?: ConversationLookupType;
|
||||
updateConfirmModal?: any;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -42,6 +46,11 @@ interface State {
|
|||
pwdLockError: string | null;
|
||||
mediaSetting: boolean | null;
|
||||
shouldLockSettings: boolean | null;
|
||||
modal: JSX.Element | null;
|
||||
}
|
||||
|
||||
interface ConfirmationDialogParams extends SessionConfirmDialogProps {
|
||||
shouldShowConfirm: () => boolean | undefined;
|
||||
}
|
||||
|
||||
interface LocalSettingType {
|
||||
|
@ -56,7 +65,7 @@ interface LocalSettingType {
|
|||
type: SessionSettingType | undefined;
|
||||
setFn: any;
|
||||
onClick: any;
|
||||
confirmationDialogParams: any | undefined;
|
||||
confirmationDialogParams: ConfirmationDialogParams | undefined;
|
||||
}
|
||||
|
||||
class SettingsViewInner extends React.Component<SettingsViewProps, State> {
|
||||
|
@ -70,6 +79,7 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
|
|||
pwdLockError: null,
|
||||
mediaSetting: null,
|
||||
shouldLockSettings: true,
|
||||
modal: null,
|
||||
};
|
||||
|
||||
this.settingsViewRef = React.createRef();
|
||||
|
@ -146,6 +156,7 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
|
|||
onSliderChange={sliderFn}
|
||||
content={content}
|
||||
confirmationDialogParams={setting.confirmationDialogParams}
|
||||
updateConfirmModal={this.props.updateConfirmModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -223,6 +234,8 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
|
|||
categoryTitle={window.i18n(`${category}SettingsTitle`)}
|
||||
/>
|
||||
|
||||
{this.state.modal ? this.state.modal : null}
|
||||
|
||||
<div className="session-settings-view">
|
||||
{shouldRenderPasswordLock ? (
|
||||
this.renderPasswordLock()
|
||||
|
@ -341,7 +354,7 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
|
|||
shouldShowConfirm: () => !window.getSettingValue('link-preview-setting'),
|
||||
title: window.i18n('linkPreviewsTitle'),
|
||||
message: window.i18n('linkPreviewsConfirmMessage'),
|
||||
okTheme: 'danger',
|
||||
okTheme: SessionButtonColor.Danger,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -499,10 +512,7 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
|
|||
buttonColor: SessionButtonColor.Primary,
|
||||
},
|
||||
onClick: () => {
|
||||
window.Whisper.events.trigger('showPasswordDialog', {
|
||||
action: 'set',
|
||||
onSuccess: this.onPasswordUpdated,
|
||||
});
|
||||
this.displayPasswordModal(PasswordAction.Set);
|
||||
},
|
||||
confirmationDialogParams: undefined,
|
||||
},
|
||||
|
@ -520,10 +530,7 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
|
|||
buttonColor: SessionButtonColor.Primary,
|
||||
},
|
||||
onClick: () => {
|
||||
window.Whisper.events.trigger('showPasswordDialog', {
|
||||
action: 'change',
|
||||
onSuccess: this.onPasswordUpdated,
|
||||
});
|
||||
this.displayPasswordModal(PasswordAction.Change);
|
||||
},
|
||||
confirmationDialogParams: undefined,
|
||||
},
|
||||
|
@ -541,16 +548,35 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
|
|||
buttonColor: SessionButtonColor.Danger,
|
||||
},
|
||||
onClick: () => {
|
||||
window.Whisper.events.trigger('showPasswordDialog', {
|
||||
action: 'remove',
|
||||
onSuccess: this.onPasswordUpdated,
|
||||
});
|
||||
this.displayPasswordModal(PasswordAction.Remove);
|
||||
},
|
||||
confirmationDialogParams: undefined,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private displayPasswordModal(passwordAction: PasswordAction) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
modal: (
|
||||
<SessionPasswordModal
|
||||
onClose={() => {
|
||||
this.clearModal();
|
||||
}}
|
||||
onOk={this.onPasswordUpdated}
|
||||
action={passwordAction}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private clearModal(): void {
|
||||
this.setState({
|
||||
...this.state,
|
||||
modal: null,
|
||||
});
|
||||
}
|
||||
|
||||
private getBlockedUserSettings(): Array<LocalSettingType> {
|
||||
const results: Array<LocalSettingType> = [];
|
||||
const blockedNumbers = BlockedNumberController.getBlockedNumbers();
|
||||
|
@ -629,5 +655,5 @@ const mapStateToProps = (state: StateType) => {
|
|||
};
|
||||
};
|
||||
|
||||
const smart = connect(mapStateToProps);
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
export const SmartSettingsView = smart(SettingsViewInner);
|
||||
|
|
|
@ -8,6 +8,9 @@ import { ConversationController } from '../session/conversations';
|
|||
import { PubKey } from '../session/types';
|
||||
import { ToastUtils } from '../session/utils';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateConfirmModal } from '../state/ducks/modalDialog';
|
||||
|
||||
export function banUser(userToBan: string, conversation?: ConversationModel) {
|
||||
let pubKeyToBan: PubKey;
|
||||
try {
|
||||
|
@ -17,25 +20,31 @@ export function banUser(userToBan: string, conversation?: ConversationModel) {
|
|||
ToastUtils.pushUserBanFailure();
|
||||
return;
|
||||
}
|
||||
window.confirmationDialog({
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const onClickClose = () => {
|
||||
dispatch(updateConfirmModal(null));
|
||||
};
|
||||
|
||||
const confirmationModalProps = {
|
||||
title: window.i18n('banUser'),
|
||||
message: window.i18n('banUserConfirm'),
|
||||
resolve: async () => {
|
||||
onClickClose,
|
||||
onClickOk: async () => {
|
||||
if (!conversation) {
|
||||
window?.log?.info('cannot ban user, the corresponding conversation was not found.');
|
||||
window.log.info('cannot ban user, the corresponding conversation was not found.');
|
||||
return;
|
||||
}
|
||||
let success = false;
|
||||
if (isOpenGroupV2(conversation.id)) {
|
||||
const roomInfos = await getV2OpenGroupRoom(conversation.id);
|
||||
if (!roomInfos) {
|
||||
window?.log?.warn('banUser room not found');
|
||||
window.log.warn('banUser room not found');
|
||||
} else {
|
||||
success = await ApiV2.banUser(pubKeyToBan, _.pick(roomInfos, 'serverUrl', 'roomId'));
|
||||
}
|
||||
} else {
|
||||
window?.log?.info('cannot ban user, the not an opengroupv2.');
|
||||
return;
|
||||
throw new Error('V1 opengroup are not supported');
|
||||
}
|
||||
if (success) {
|
||||
ToastUtils.pushUserBanSuccess();
|
||||
|
@ -43,7 +52,9 @@ export function banUser(userToBan: string, conversation?: ConversationModel) {
|
|||
ToastUtils.pushUserBanFailure();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
dispatch(updateConfirmModal(confirmationModalProps));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -64,31 +75,40 @@ export function unbanUser(userToUnBan: string, conversation?: ConversationModel)
|
|||
ToastUtils.pushUserBanFailure();
|
||||
return;
|
||||
}
|
||||
window.confirmationDialog({
|
||||
title: window.i18n('unbanUser'),
|
||||
message: window.i18n('unbanUserConfirm'),
|
||||
resolve: async () => {
|
||||
if (!conversation) {
|
||||
// double check here. the convo might have been removed since the dialog was opened
|
||||
window?.log?.info('cannot unban user, the corresponding conversation was not found.');
|
||||
return;
|
||||
}
|
||||
let success = false;
|
||||
if (isOpenGroupV2(conversation.id)) {
|
||||
const roomInfos = await getV2OpenGroupRoom(conversation.id);
|
||||
if (!roomInfos) {
|
||||
window?.log?.warn('unbanUser room not found');
|
||||
} else {
|
||||
success = await ApiV2.unbanUser(pubKeyToUnban, _.pick(roomInfos, 'serverUrl', 'roomId'));
|
||||
}
|
||||
}
|
||||
if (success) {
|
||||
ToastUtils.pushUserUnbanSuccess();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const onClickClose = () => dispatch(updateConfirmModal(null));
|
||||
|
||||
const onClickOk = async () => {
|
||||
if (!conversation) {
|
||||
// double check here. the convo might have been removed since the dialog was opened
|
||||
window.log.info('cannot unban user, the corresponding conversation was not found.');
|
||||
return;
|
||||
}
|
||||
let success = false;
|
||||
if (isOpenGroupV2(conversation.id)) {
|
||||
const roomInfos = await getV2OpenGroupRoom(conversation.id);
|
||||
if (!roomInfos) {
|
||||
window.log.warn('unbanUser room not found');
|
||||
} else {
|
||||
ToastUtils.pushUserUnbanFailure();
|
||||
success = await ApiV2.unbanUser(pubKeyToUnban, _.pick(roomInfos, 'serverUrl', 'roomId'));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
if (success) {
|
||||
ToastUtils.pushUserUnbanSuccess();
|
||||
} else {
|
||||
ToastUtils.pushUserUnbanFailure();
|
||||
}
|
||||
};
|
||||
|
||||
dispatch(
|
||||
updateConfirmModal({
|
||||
title: window.i18n('unbanUser'),
|
||||
message: window.i18n('unbanUserConfirm'),
|
||||
onClickOk,
|
||||
onClickClose,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function copyBodyToClipboard(body?: string) {
|
||||
|
@ -145,11 +165,21 @@ export async function addSenderAsModerator(sender: string, convoId: string) {
|
|||
}
|
||||
|
||||
const acceptOpenGroupInvitationV2 = (completeUrl: string, roomName?: string) => {
|
||||
window.confirmationDialog({
|
||||
title: window.i18n('joinOpenGroupAfterInvitationConfirmationTitle', roomName),
|
||||
message: window.i18n('joinOpenGroupAfterInvitationConfirmationDesc', roomName),
|
||||
resolve: () => joinOpenGroupV2WithUIEvents(completeUrl, true, false),
|
||||
});
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onClickClose = () => {
|
||||
dispatch(updateConfirmModal(null));
|
||||
};
|
||||
|
||||
dispatch(
|
||||
updateConfirmModal({
|
||||
title: window.i18n('joinOpenGroupAfterInvitationConfirmationTitle', roomName),
|
||||
message: window.i18n('joinOpenGroupAfterInvitationConfirmationDesc', roomName),
|
||||
onClickOk: () => joinOpenGroupV2WithUIEvents(completeUrl, true, false),
|
||||
|
||||
onClickClose,
|
||||
})
|
||||
);
|
||||
// this function does not throw, and will showToasts if anything happens
|
||||
};
|
||||
|
||||
|
|
1
ts/ip2country.d.ts
vendored
Normal file
1
ts/ip2country.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module 'ip2country';
|
|
@ -41,6 +41,8 @@ import { OpenGroupVisibleMessage } from '../session/messages/outgoing/visibleMes
|
|||
import { OpenGroupRequestCommonType } from '../opengroup/opengroupV2/ApiUtil';
|
||||
import { getOpenGroupV2FromConversationId } from '../opengroup/utils/OpenGroupUtils';
|
||||
import { NotificationForConvoOption } from '../components/conversation/ConversationHeader';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateConfirmModal } from '../state/ducks/modalDialog';
|
||||
|
||||
export enum ConversationTypeEnum {
|
||||
GROUP = 'group',
|
||||
|
@ -427,8 +429,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
onBlockContact: this.block,
|
||||
onUnblockContact: this.unblock,
|
||||
onCopyPublicKey: this.copyPublicKey,
|
||||
onDeleteContact: this.deleteContact,
|
||||
onChangeNickname: this.changeNickname,
|
||||
onClearNickname: this.clearNickname,
|
||||
onDeleteMessages: this.deleteMessages,
|
||||
onLeaveGroup: () => {
|
||||
|
@ -1234,38 +1234,27 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
void ConversationInteraction.copyPublicKey(this.id);
|
||||
}
|
||||
|
||||
public changeNickname() {
|
||||
if (this.isGroup()) {
|
||||
throw new Error(
|
||||
'Called changeNickname() on a group. This is only supported in 1-on-1 conversation items and 1-on-1 conversation headers'
|
||||
);
|
||||
}
|
||||
window.showNicknameDialog({
|
||||
convoId: this.id,
|
||||
});
|
||||
}
|
||||
|
||||
public clearNickname = () => {
|
||||
void this.setNickname('');
|
||||
};
|
||||
|
||||
public deleteContact() {
|
||||
let title = window.i18n('delete');
|
||||
let message = window.i18n('deleteContactConfirmation');
|
||||
// public deleteContact() {
|
||||
// let title = window.i18n('delete');
|
||||
// let message = window.i18n('deleteContactConfirmation');
|
||||
|
||||
if (this.isGroup()) {
|
||||
title = window.i18n('leaveGroup');
|
||||
message = window.i18n('leaveGroupConfirmation');
|
||||
}
|
||||
// if (this.isGroup()) {
|
||||
// title = window.i18n('leaveGroup');
|
||||
// message = window.i18n('leaveGroupConfirmation');
|
||||
// }
|
||||
|
||||
window.confirmationDialog({
|
||||
title,
|
||||
message,
|
||||
resolve: () => {
|
||||
void ConversationController.getInstance().deleteContact(this.id);
|
||||
},
|
||||
});
|
||||
}
|
||||
// window.confirmationDialog({
|
||||
// title,
|
||||
// message,
|
||||
// resolve: () => {
|
||||
// void ConversationController.getInstance().deleteContact(this.id);
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
public async removeMessage(messageId: any) {
|
||||
await dataRemoveMessage(messageId);
|
||||
|
@ -1291,7 +1280,16 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
};
|
||||
}
|
||||
|
||||
window.confirmationDialog(params);
|
||||
// window.confirmationDialog(params);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
dispatch(
|
||||
updateConfirmModal({
|
||||
title: window.i18n('deleteMessages'),
|
||||
message: window.i18n('deleteConversationConfirmation'),
|
||||
onClickOk: () => this.destroyMessages(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async destroyMessages() {
|
||||
|
|
|
@ -253,6 +253,8 @@ export interface MessageRegularProps {
|
|||
onCopyPubKey?: () => void;
|
||||
onBanUser?: () => void;
|
||||
onUnbanUser?: () => void;
|
||||
// setModal?: any;
|
||||
updateSessionConversationModal: (modal: JSX.Element | null) => any;
|
||||
|
||||
onShowDetail: () => void;
|
||||
onShowUserDetails: (userPubKey: string) => void;
|
||||
|
|
|
@ -36,6 +36,8 @@ import { actions as conversationActions } from '../state/ducks/conversations';
|
|||
import { SwarmPolling } from '../session/snode_api/swarmPolling';
|
||||
import { MessageModel } from '../models/message';
|
||||
|
||||
import { updateConfirmModal } from '../state/ducks/modalDialog';
|
||||
|
||||
export const distributingClosedGroupEncryptionKeyPairs = new Map<string, ECKeyPair>();
|
||||
|
||||
export async function handleClosedGroupControlMessage(
|
||||
|
@ -969,54 +971,65 @@ async function sendToGroupMembers(
|
|||
const inviteResults = await Promise.all(promises);
|
||||
const allInvitesSent = _.every(inviteResults, Boolean);
|
||||
|
||||
console.log('@@@@', inviteResults);
|
||||
throw new Error('audric: TODEBUG');
|
||||
|
||||
if (allInvitesSent) {
|
||||
// if (true) {
|
||||
if (isRetry) {
|
||||
const invitesTitle =
|
||||
inviteResults.length > 1
|
||||
? window.i18n('closedGroupInviteSuccessTitlePlural')
|
||||
: window.i18n('closedGroupInviteSuccessTitle');
|
||||
window.confirmationDialog({
|
||||
title: invitesTitle,
|
||||
message: window.i18n('closedGroupInviteSuccessMessage'),
|
||||
});
|
||||
|
||||
window.inboxStore?.dispatch(
|
||||
updateConfirmModal({
|
||||
title: invitesTitle,
|
||||
message: window.i18n('closedGroupInviteSuccessMessage'),
|
||||
hideCancel: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
return allInvitesSent;
|
||||
} else {
|
||||
// Confirmation dialog that recursively calls sendToGroupMembers on resolve
|
||||
window.confirmationDialog({
|
||||
title:
|
||||
inviteResults.length > 1
|
||||
? window.i18n('closedGroupInviteFailTitlePlural')
|
||||
: window.i18n('closedGroupInviteFailTitle'),
|
||||
message:
|
||||
inviteResults.length > 1
|
||||
? window.i18n('closedGroupInviteFailMessagePlural')
|
||||
: window.i18n('closedGroupInviteFailMessage'),
|
||||
okText: window.i18n('closedGroupInviteOkText'),
|
||||
resolve: async () => {
|
||||
const membersToResend: Array<string> = new Array<string>();
|
||||
inviteResults.forEach((result, index) => {
|
||||
const member = listOfMembers[index];
|
||||
// group invite must always contain the admin member.
|
||||
if (result !== true || admins.includes(member)) {
|
||||
membersToResend.push(member);
|
||||
|
||||
window.inboxStore?.dispatch(
|
||||
updateConfirmModal({
|
||||
title:
|
||||
inviteResults.length > 1
|
||||
? window.i18n('closedGroupInviteFailTitlePlural')
|
||||
: window.i18n('closedGroupInviteFailTitle'),
|
||||
message:
|
||||
inviteResults.length > 1
|
||||
? window.i18n('closedGroupInviteFailMessagePlural')
|
||||
: window.i18n('closedGroupInviteFailMessage'),
|
||||
okText: window.i18n('closedGroupInviteOkText'),
|
||||
onClickOk: async () => {
|
||||
const membersToResend: Array<string> = new Array<string>();
|
||||
inviteResults.forEach((result, index) => {
|
||||
const member = listOfMembers[index];
|
||||
// group invite must always contain the admin member.
|
||||
if (result !== true || admins.includes(member)) {
|
||||
membersToResend.push(member);
|
||||
}
|
||||
});
|
||||
if (membersToResend.length > 0) {
|
||||
const isRetrySend = true;
|
||||
await sendToGroupMembers(
|
||||
membersToResend,
|
||||
groupPublicKey,
|
||||
groupName,
|
||||
admins,
|
||||
encryptionKeyPair,
|
||||
dbMessage,
|
||||
existingExpireTimer,
|
||||
isRetrySend
|
||||
);
|
||||
}
|
||||
});
|
||||
if (membersToResend.length > 0) {
|
||||
const isRetrySend = true;
|
||||
await sendToGroupMembers(
|
||||
membersToResend,
|
||||
groupPublicKey,
|
||||
groupName,
|
||||
admins,
|
||||
encryptionKeyPair,
|
||||
dbMessage,
|
||||
existingExpireTimer,
|
||||
isRetrySend
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
return allInvitesSent;
|
||||
}
|
||||
|
|
|
@ -52,6 +52,9 @@ export const UI = {
|
|||
WHITE_PALE: '#AFAFAF',
|
||||
GREEN: '#00F782',
|
||||
|
||||
// CAUTION
|
||||
WARNING: '#FFC02E',
|
||||
|
||||
// SEMANTIC COLORS
|
||||
DANGER: '#FF453A',
|
||||
DANGER_ALT: '#FF4538',
|
||||
|
|
|
@ -1,3 +1,317 @@
|
|||
import * as OnionPaths from './onionPath';
|
||||
|
||||
<<<<<<< HEAD
|
||||
export { OnionPaths };
|
||||
=======
|
||||
import { updateOnionPaths } from '../../state/ducks/onion';
|
||||
|
||||
export type Snode = SnodePool.Snode;
|
||||
|
||||
const desiredGuardCount = 3;
|
||||
const minimumGuardCount = 2;
|
||||
export interface SnodePath {
|
||||
path: Array<Snode>;
|
||||
bad: boolean;
|
||||
}
|
||||
|
||||
export class OnionPaths {
|
||||
private static instance: OnionPaths | null;
|
||||
private static readonly onionRequestHops = 3;
|
||||
private onionPaths: Array<SnodePath> = [];
|
||||
|
||||
// This array is meant to store nodes will full info,
|
||||
// so using GuardNode would not be correct (there is
|
||||
// some naming issue here it seems)
|
||||
private guardNodes: Array<Snode> = [];
|
||||
private onionRequestCounter = 0; // Request index for debugging
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance() {
|
||||
if (OnionPaths.instance) {
|
||||
return OnionPaths.instance;
|
||||
}
|
||||
OnionPaths.instance = new OnionPaths();
|
||||
return OnionPaths.instance;
|
||||
}
|
||||
|
||||
public async buildNewOnionPaths() {
|
||||
// this function may be called concurrently make sure we only have one inflight
|
||||
return allowOnlyOneAtATime('buildNewOnionPaths', async () => {
|
||||
await this.buildNewOnionPathsWorker();
|
||||
});
|
||||
}
|
||||
|
||||
public async getOnionPath(toExclude?: { pubkey_ed25519: string }): Promise<Array<Snode>> {
|
||||
const { log, CONSTANTS } = window;
|
||||
|
||||
let goodPaths = this.onionPaths.filter(x => !x.bad);
|
||||
|
||||
let attemptNumber = 0;
|
||||
while (goodPaths.length < minimumGuardCount) {
|
||||
log.error(
|
||||
`Must have at least 2 good onion paths, actual: ${goodPaths.length}, attempt #${attemptNumber} fetching more...`
|
||||
);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this.buildNewOnionPaths();
|
||||
// should we add a delay? buildNewOnionPaths should act as one
|
||||
|
||||
// reload goodPaths now
|
||||
attemptNumber += 1;
|
||||
goodPaths = this.onionPaths.filter(x => !x.bad);
|
||||
}
|
||||
|
||||
if (goodPaths.length <= 0) {
|
||||
window.inboxStore?.dispatch(updateOnionPaths({ path: new Array<Snode>(), bad: true }));
|
||||
} else {
|
||||
window.inboxStore?.dispatch(updateOnionPaths(goodPaths[0]));
|
||||
}
|
||||
|
||||
const paths = _.shuffle(goodPaths);
|
||||
|
||||
if (!toExclude) {
|
||||
if (!paths[0]) {
|
||||
log.error('LokiSnodeAPI::getOnionPath - no path in', paths);
|
||||
return [];
|
||||
}
|
||||
if (!paths[0].path) {
|
||||
log.error('LokiSnodeAPI::getOnionPath - no path in', paths[0]);
|
||||
}
|
||||
return paths[0].path;
|
||||
}
|
||||
|
||||
// Select a path that doesn't contain `toExclude`
|
||||
const otherPaths = paths.filter(
|
||||
path => !_.some(path.path, node => node.pubkey_ed25519 === toExclude.pubkey_ed25519)
|
||||
);
|
||||
|
||||
if (otherPaths.length === 0) {
|
||||
// This should never happen!
|
||||
// well it did happen, should we
|
||||
// await this.buildNewOnionPaths();
|
||||
// and restart call?
|
||||
log.error(
|
||||
'LokiSnodeAPI::getOnionPath - no paths without',
|
||||
toExclude.pubkey_ed25519,
|
||||
'path count',
|
||||
paths.length,
|
||||
'goodPath count',
|
||||
goodPaths.length,
|
||||
'paths',
|
||||
paths
|
||||
);
|
||||
throw new Error('No onion paths available after filtering');
|
||||
}
|
||||
|
||||
if (!otherPaths[0].path) {
|
||||
log.error('LokiSnodeAPI::getOnionPath - otherPaths no path in', otherPaths[0]);
|
||||
}
|
||||
|
||||
return otherPaths[0].path;
|
||||
}
|
||||
|
||||
public hasOnionPath(): boolean {
|
||||
// returns true if there exists a valid onion path
|
||||
return this.onionPaths.length !== 0 && this.onionPaths[0].path.length !== 0;
|
||||
}
|
||||
|
||||
public getOnionPathNoRebuild() {
|
||||
return this.onionPaths ? this.onionPaths[0].path : [];
|
||||
}
|
||||
|
||||
public markPathAsBad(path: Array<Snode>) {
|
||||
// TODO: we might want to remove the nodes from the
|
||||
// node pool (but we don't know which node on the path
|
||||
// is causing issues)
|
||||
|
||||
this.onionPaths.forEach(p => {
|
||||
if (_.isEqual(p.path, path)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
p.bad = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public assignOnionRequestNumber() {
|
||||
this.onionRequestCounter += 1;
|
||||
return this.onionRequestCounter;
|
||||
}
|
||||
|
||||
private async testGuardNode(snode: Snode) {
|
||||
const { log } = window;
|
||||
|
||||
log.info('Testing a candidate guard node ', snode);
|
||||
|
||||
// Send a post request and make sure it is OK
|
||||
const endpoint = '/storage_rpc/v1';
|
||||
|
||||
const url = `https://${snode.ip}:${snode.port}${endpoint}`;
|
||||
|
||||
const ourPK = UserUtils.getOurPubKeyStrFromCache();
|
||||
const pubKey = window.getStoragePubKey(ourPK); // truncate if testnet
|
||||
|
||||
const method = 'get_snodes_for_pubkey';
|
||||
const params = { pubKey };
|
||||
const body = {
|
||||
jsonrpc: '2.0',
|
||||
id: '0',
|
||||
method,
|
||||
params,
|
||||
};
|
||||
|
||||
const fetchOptions = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: 10000, // 10s, we want a smaller timeout for testing
|
||||
agent: snodeHttpsAgent,
|
||||
};
|
||||
|
||||
let response;
|
||||
|
||||
try {
|
||||
// Log this line for testing
|
||||
// curl -k -X POST -H 'Content-Type: application/json' -d '"+fetchOptions.body.replace(/"/g, "\\'")+"'", url
|
||||
window.log.info('insecureNodeFetch => plaintext for testGuardNode');
|
||||
|
||||
response = await insecureNodeFetch(url, fetchOptions);
|
||||
} catch (e) {
|
||||
if (e.type === 'request-timeout') {
|
||||
log.warn('test timeout for node,', snode);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const tg = await response.text();
|
||||
log.info('Node failed the guard test:', snode);
|
||||
}
|
||||
|
||||
return response.ok;
|
||||
}
|
||||
|
||||
private async selectGuardNodes(): Promise<Array<Snode>> {
|
||||
const { log } = window;
|
||||
|
||||
// `getRandomSnodePool` is expected to refresh itself on low nodes
|
||||
const nodePool = await SnodePool.getRandomSnodePool();
|
||||
if (nodePool.length < desiredGuardCount) {
|
||||
log.error('Could not select guard nodes. Not enough nodes in the pool: ', nodePool.length);
|
||||
return [];
|
||||
}
|
||||
|
||||
const shuffled = _.shuffle(nodePool);
|
||||
|
||||
let guardNodes: Array<Snode> = [];
|
||||
|
||||
console.log('@@@@ guardNodes: ', guardNodes);
|
||||
|
||||
// The use of await inside while is intentional:
|
||||
// we only want to repeat if the await fails
|
||||
// eslint-disable-next-line-no-await-in-loop
|
||||
while (guardNodes.length < 3) {
|
||||
if (shuffled.length < desiredGuardCount) {
|
||||
log.error('Not enought nodes in the pool');
|
||||
break;
|
||||
}
|
||||
|
||||
const candidateNodes = shuffled.splice(0, desiredGuardCount);
|
||||
|
||||
// Test all three nodes at once
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const idxOk = await Promise.all(candidateNodes.map(n => this.testGuardNode(n)));
|
||||
|
||||
const goodNodes = _.zip(idxOk, candidateNodes)
|
||||
.filter(x => x[0])
|
||||
.map(x => x[1]) as Array<Snode>;
|
||||
|
||||
guardNodes = _.concat(guardNodes, goodNodes);
|
||||
}
|
||||
|
||||
if (guardNodes.length < desiredGuardCount) {
|
||||
log.error(`COULD NOT get enough guard nodes, only have: ${guardNodes.length}`);
|
||||
}
|
||||
|
||||
log.info('new guard nodes: ', guardNodes);
|
||||
|
||||
const edKeys = guardNodes.map(n => n.pubkey_ed25519);
|
||||
|
||||
await updateGuardNodes(edKeys);
|
||||
|
||||
return guardNodes;
|
||||
}
|
||||
|
||||
private async buildNewOnionPathsWorker() {
|
||||
const { log } = window;
|
||||
|
||||
log.info('LokiSnodeAPI::buildNewOnionPaths - building new onion paths');
|
||||
|
||||
const allNodes = await SnodePool.getRandomSnodePool();
|
||||
|
||||
if (this.guardNodes.length === 0) {
|
||||
// Not cached, load from DB
|
||||
const nodes = await getGuardNodes();
|
||||
|
||||
if (nodes.length === 0) {
|
||||
log.warn(
|
||||
'LokiSnodeAPI::buildNewOnionPaths - no guard nodes in DB. Will be selecting new guards nodes...'
|
||||
);
|
||||
} else {
|
||||
// We only store the nodes' keys, need to find full entries:
|
||||
const edKeys = nodes.map(x => x.ed25519PubKey);
|
||||
this.guardNodes = allNodes.filter(x => edKeys.indexOf(x.pubkey_ed25519) !== -1);
|
||||
|
||||
if (this.guardNodes.length < edKeys.length) {
|
||||
log.warn(
|
||||
`LokiSnodeAPI::buildNewOnionPaths - could not find some guard nodes: ${this.guardNodes.length}/${edKeys.length} left`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If guard nodes is still empty (the old nodes are now invalid), select new ones:
|
||||
if (this.guardNodes.length < minimumGuardCount) {
|
||||
// TODO: don't throw away potentially good guard nodes
|
||||
this.guardNodes = await this.selectGuardNodes();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: select one guard node and 2 other nodes randomly
|
||||
let otherNodes = _.difference(allNodes, this.guardNodes);
|
||||
|
||||
if (otherNodes.length < 2) {
|
||||
log.warn(
|
||||
'LokiSnodeAPI::buildNewOnionPaths - Too few nodes to build an onion path! Refreshing pool and retrying'
|
||||
);
|
||||
await SnodePool.refreshRandomPool();
|
||||
await this.buildNewOnionPaths();
|
||||
return;
|
||||
}
|
||||
|
||||
otherNodes = _.shuffle(otherNodes);
|
||||
const guards = _.shuffle(this.guardNodes);
|
||||
|
||||
// Create path for every guard node:
|
||||
const nodesNeededPerPaths = OnionPaths.onionRequestHops - 1;
|
||||
|
||||
// Each path needs X (nodesNeededPerPaths) nodes in addition to the guard node:
|
||||
const maxPath = Math.floor(
|
||||
Math.min(
|
||||
guards.length,
|
||||
nodesNeededPerPaths ? otherNodes.length / nodesNeededPerPaths : otherNodes.length
|
||||
)
|
||||
);
|
||||
|
||||
// TODO: might want to keep some of the existing paths
|
||||
this.onionPaths = [];
|
||||
|
||||
for (let i = 0; i < maxPath; i += 1) {
|
||||
const path = [guards[i]];
|
||||
for (let j = 0; j < nodesNeededPerPaths; j += 1) {
|
||||
path.push(otherNodes[i * nodesNeededPerPaths + j]);
|
||||
}
|
||||
this.onionPaths.push({ path, bad: false });
|
||||
}
|
||||
|
||||
log.info(`Built ${this.onionPaths.length} onion paths`);
|
||||
}
|
||||
}
|
||||
>>>>>>> w/onion-paths
|
||||
|
|
|
@ -11,6 +11,7 @@ const desiredGuardCount = 3;
|
|||
const minimumGuardCount = 2;
|
||||
|
||||
export type SnodePath = Array<Snode>;
|
||||
import { updateOnionPaths } from '../../state/ducks/onion';
|
||||
|
||||
const onionRequestHops = 3;
|
||||
let onionPaths: Array<SnodePath> = [];
|
||||
|
@ -133,6 +134,12 @@ export async function getOnionPath(toExclude?: Snode): Promise<Array<Snode>> {
|
|||
attemptNumber += 1;
|
||||
}
|
||||
|
||||
if (onionPaths.length <= 0) {
|
||||
window.inboxStore?.dispatch(updateOnionPaths({ path: new Array<Snode>(), bad: true }));
|
||||
} else {
|
||||
window.inboxStore?.dispatch(updateOnionPaths(goodPaths[0]));
|
||||
}
|
||||
|
||||
const onionPathsWithoutExcluded = toExclude
|
||||
? onionPaths.filter(
|
||||
path => !_.some(path, node => node.pubkey_ed25519 === toExclude.pubkey_ed25519)
|
||||
|
|
|
@ -5,6 +5,7 @@ import { actions as conversations } from './ducks/conversations';
|
|||
import { actions as user } from './ducks/user';
|
||||
import { actions as sections } from './ducks/section';
|
||||
import { actions as theme } from './ducks/theme';
|
||||
import { actions as modalDialog } from './ducks/modalDialog';
|
||||
|
||||
export function mapDispatchToProps(dispatch: Dispatch): Object {
|
||||
return {
|
||||
|
@ -15,6 +16,7 @@ export function mapDispatchToProps(dispatch: Dispatch): Object {
|
|||
...user,
|
||||
...theme,
|
||||
...sections,
|
||||
...modalDialog
|
||||
},
|
||||
dispatch
|
||||
),
|
||||
|
|
|
@ -3,9 +3,11 @@ import React from 'react';
|
|||
// import 'reset-css/reset.css';
|
||||
|
||||
import { DefaultTheme, ThemeProvider } from 'styled-components';
|
||||
import { pushToastWarning } from '../../session/utils/Toast';
|
||||
|
||||
const white = '#ffffff';
|
||||
const black = '#000000';
|
||||
const warning = '#e7b100';
|
||||
const destructive = '#ff453a';
|
||||
const accentLightTheme = '#00e97b';
|
||||
const accentDarkTheme = '#00f782';
|
||||
|
@ -40,6 +42,7 @@ export const lightTheme: DefaultTheme = {
|
|||
colors: {
|
||||
accent: accentLightTheme,
|
||||
accentButton: black,
|
||||
warning: warning,
|
||||
destructive: destructive,
|
||||
cellBackground: '#fcfcfc',
|
||||
modalBackground: '#fcfcfc',
|
||||
|
@ -95,6 +98,7 @@ export const darkTheme = {
|
|||
colors: {
|
||||
accent: accentDarkTheme,
|
||||
accentButton: accentDarkTheme,
|
||||
warning: warning,
|
||||
destructive: destructive,
|
||||
cellBackground: '#1b1b1b',
|
||||
modalBackground: '#101011',
|
||||
|
|
|
@ -82,6 +82,18 @@ export interface ConversationType {
|
|||
avatarPath?: string; // absolute filepath to the avatar
|
||||
groupAdmins?: Array<string>; // admins for closed groups and moderators for open groups
|
||||
members?: Array<string>; // members for closed groups only
|
||||
|
||||
onClick?: () => void;
|
||||
onBlockContact?: () => void;
|
||||
onUnblockContact?: () => void;
|
||||
onCopyPublicKey?: () => void;
|
||||
onDeleteContact?: () => void;
|
||||
onLeaveGroup?: () => void;
|
||||
onDeleteMessages?: () => void;
|
||||
onInviteContacts?: () => void;
|
||||
onMarkAllRead?: () => void;
|
||||
onClearNickname?: () => void;
|
||||
onChangeNickname?: () => void;
|
||||
}
|
||||
|
||||
export type ConversationLookupType = {
|
||||
|
|
21
ts/state/ducks/modalDialog.tsx
Normal file
21
ts/state/ducks/modalDialog.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { SessionConfirmDialogProps } from '../../components/session/SessionConfirm';
|
||||
|
||||
export type ConfirmModalState = SessionConfirmDialogProps | null;
|
||||
|
||||
const initialState: ConfirmModalState = null as ConfirmModalState;
|
||||
|
||||
const confirmModalSlice = createSlice({
|
||||
name: 'confirmModal',
|
||||
initialState,
|
||||
reducers: {
|
||||
updateConfirmModal(state, action: PayloadAction<ConfirmModalState | null>) {
|
||||
state = action.payload;
|
||||
return action.payload;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const { actions, reducer } = confirmModalSlice;
|
||||
export const { updateConfirmModal } = actions;
|
||||
export const confirmModalReducer = reducer;
|
35
ts/state/ducks/onion.tsx
Normal file
35
ts/state/ducks/onion.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { SnodePath, Snode } from '../../session/onions/index';
|
||||
|
||||
export type OnionState = {
|
||||
snodePath: SnodePath;
|
||||
};
|
||||
|
||||
|
||||
const initialState = {
|
||||
snodePath: {
|
||||
path: new Array<Snode>(),
|
||||
bad: false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* This slice is the one holding the default joinable rooms fetched once in a while from the default opengroup v2 server.
|
||||
*/
|
||||
const onionSlice = createSlice({
|
||||
name: 'onionPaths',
|
||||
initialState,
|
||||
reducers: {
|
||||
updateOnionPaths(state, action: PayloadAction<SnodePath>) {
|
||||
let newPayload = { snodePath: action.payload };
|
||||
|
||||
let isEqual = JSON.stringify(state, null, 2) == JSON.stringify(newPayload, null, 2);
|
||||
return isEqual ? state : newPayload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// destructures
|
||||
const { actions, reducer } = onionSlice;
|
||||
export const { updateOnionPaths } = actions;
|
||||
export const defaultOnionReducer = reducer;
|
|
@ -10,6 +10,8 @@ import {
|
|||
defaultMentionsInputReducer as mentionsInput,
|
||||
MentionsInputState,
|
||||
} from './ducks/mentionsInput';
|
||||
import { defaultOnionReducer as onionPaths, OnionState } from './ducks/onion';
|
||||
import { confirmModalReducer as confirmModal, ConfirmModalState } from './ducks/modalDialog';
|
||||
|
||||
export type StateType = {
|
||||
search: SearchStateType;
|
||||
|
@ -20,6 +22,11 @@ export type StateType = {
|
|||
section: SectionStateType;
|
||||
defaultRooms: DefaultRoomsState;
|
||||
mentionsInput: MentionsInputState;
|
||||
|
||||
onionPaths: OnionState;
|
||||
|
||||
confirmModal: ConfirmModalState;
|
||||
// modalState: ConfirmModalState
|
||||
};
|
||||
|
||||
export const reducers = {
|
||||
|
@ -33,6 +40,8 @@ export const reducers = {
|
|||
section,
|
||||
defaultRooms,
|
||||
mentionsInput,
|
||||
onionPaths,
|
||||
confirmModal,
|
||||
};
|
||||
|
||||
// Making this work would require that our reducer signature supported AnyAction, not
|
||||
|
|
|
@ -17,6 +17,7 @@ const mapStateToProps = (state: StateType) => {
|
|||
theme: getTheme(state),
|
||||
messages: getMessagesOfSelectedConversation(state),
|
||||
ourNumber: getOurNumber(state),
|
||||
confirmModal: (state: StateType) => {state.confirmModal}
|
||||
};
|
||||
};
|
||||
|
||||
|
|
1
ts/styled.d.ts
vendored
1
ts/styled.d.ts
vendored
|
@ -26,6 +26,7 @@ declare module 'styled-components' {
|
|||
colors: {
|
||||
accent: string;
|
||||
accentButton: string;
|
||||
warning: string;
|
||||
destructive: string;
|
||||
cellBackground: string;
|
||||
modalBackground: string;
|
||||
|
|
|
@ -112,7 +112,15 @@ export async function getFile(attachment: StagedAttachmentType, maxMeasurements?
|
|||
};
|
||||
}
|
||||
|
||||
export async function readFile(attachment: any): Promise<object> {
|
||||
export type AttachmentFileType = {
|
||||
attachment: any;
|
||||
data: ArrayBuffer;
|
||||
size: number;
|
||||
}
|
||||
|
||||
|
||||
// export async function readFile(attachment: any): Promise<object> {
|
||||
export async function readFile(attachment: any): Promise<AttachmentFileType> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const FR = new FileReader();
|
||||
FR.onload = e => {
|
||||
|
|
7
ts/window.d.ts
vendored
7
ts/window.d.ts
vendored
|
@ -13,8 +13,9 @@ import { Store } from 'redux';
|
|||
import { MessageController } from './session/messages/MessageController';
|
||||
import { DefaultTheme } from 'styled-components';
|
||||
|
||||
import { ConversationCollection } from './models/conversation';
|
||||
import { ConversationCollection, ConversationModel } from './models/conversation';
|
||||
import { ConversationType } from './state/ducks/conversations';
|
||||
import { ConversationController } from './session/conversations';
|
||||
|
||||
/*
|
||||
We declare window stuff here instead of global.d.ts because we are importing other declarations.
|
||||
|
@ -35,7 +36,6 @@ declare global {
|
|||
Whisper: any;
|
||||
clearLocalData: any;
|
||||
clipboard: any;
|
||||
confirmationDialog: (params: ConfirmationDialogParams) => any;
|
||||
dcodeIO: any;
|
||||
displayNameRegex: any;
|
||||
friends: any;
|
||||
|
@ -60,8 +60,6 @@ declare global {
|
|||
getSeedNodeList: () => Array<any> | undefined;
|
||||
setPassword: any;
|
||||
setSettingValue: any;
|
||||
showEditProfileDialog: any;
|
||||
showNicknameDialog: (options: { convoId: string }) => void;
|
||||
showResetSessionIdDialog: any;
|
||||
storage: any;
|
||||
textsecure: LibTextsecure;
|
||||
|
@ -89,5 +87,6 @@ declare global {
|
|||
darkTheme: DefaultTheme;
|
||||
LokiPushNotificationServer: any;
|
||||
globalOnlineStatus: boolean;
|
||||
confirmationDialog: any;
|
||||
}
|
||||
}
|
||||
|
|
65
yarn.lock
65
yarn.lock
|
@ -695,6 +695,15 @@
|
|||
"@types/prop-types" "*"
|
||||
csstype "^2.2.0"
|
||||
|
||||
"@types/react@^16.8.3":
|
||||
version "16.14.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.7.tgz#b62bd8cc4675d6fe3976126cdd208deda267f1fb"
|
||||
integrity sha512-JhbikeQ1i18ut9Sro3j/xvhN073zJA9sqGqbwJByhOfLPXxEJdqjal9piZd9tmVEC+LP6RN2dLvWx9Hbr0reTw==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/redux-logger@3.0.7":
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/redux-logger/-/redux-logger-3.0.7.tgz#163f6f6865c69c21d56f9356dc8d741718ec0db0"
|
||||
|
@ -715,6 +724,11 @@
|
|||
"@types/glob" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/scheduler@*":
|
||||
version "0.16.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
|
||||
integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
|
||||
|
||||
"@types/semver@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45"
|
||||
|
@ -1249,6 +1263,14 @@ asar@0.14.0:
|
|||
mksnapshot "^0.3.0"
|
||||
tmp "0.0.28"
|
||||
|
||||
asbycountry@^1.4.2:
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/asbycountry/-/asbycountry-1.4.2.tgz#26bf0e090225b93f7d1fc5a177899c900b5c8258"
|
||||
integrity sha512-NnIJ1lUYJ/M0XmoOA1T5uLQWbD81MDz5MpwufSHymw8j3DauFyTDki7ixxG8nMeUo5GBkFT1U/USOcz0mJnrNQ==
|
||||
dependencies:
|
||||
chalk "^1.1.3"
|
||||
fetch "^1.1.0"
|
||||
|
||||
asn1.js@^4.0.0:
|
||||
version "4.10.1"
|
||||
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
|
||||
|
@ -1563,6 +1585,13 @@ bindings@^1.5.0:
|
|||
dependencies:
|
||||
file-uri-to-path "1.0.0"
|
||||
|
||||
biskviit@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/biskviit/-/biskviit-1.0.1.tgz#037a0cd4b71b9e331fd90a1122de17dc49e420a7"
|
||||
integrity sha1-A3oM1LcbnjMf2QoRIt4X3EnkIKc=
|
||||
dependencies:
|
||||
psl "^1.1.7"
|
||||
|
||||
bl@^1.0.0:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c"
|
||||
|
@ -2766,6 +2795,11 @@ cosmiconfig@^7.0.0:
|
|||
path-type "^4.0.0"
|
||||
yaml "^1.10.0"
|
||||
|
||||
country-code-lookup@^0.0.19:
|
||||
version "0.0.19"
|
||||
resolved "https://registry.yarnpkg.com/country-code-lookup/-/country-code-lookup-0.0.19.tgz#3fbf0192758ecf0d5eee0efbc220d62706c50fd6"
|
||||
integrity sha512-lpvgdPyj8RuP0CSZhACNf5ueKlLbv/IQUAQfg7yr/qJbFrdcWV7Y+aDN9K/u/bx3MXRfcsjuW+TdIc0AEj7kDw==
|
||||
|
||||
crc32-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-2.0.0.tgz#e3cdd3b4df3168dd74e3de3fbbcb7b297fe908f4"
|
||||
|
@ -3715,6 +3749,13 @@ encodeurl@^1.0.2, encodeurl@~1.0.2:
|
|||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
|
||||
|
||||
encoding@0.1.12:
|
||||
version "0.1.12"
|
||||
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
|
||||
integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=
|
||||
dependencies:
|
||||
iconv-lite "~0.4.13"
|
||||
|
||||
end-of-stream@^1.0.0, end-of-stream@^1.1.0:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||
|
@ -4319,6 +4360,14 @@ fd-slicer@~1.1.0:
|
|||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
fetch@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fetch/-/fetch-1.1.0.tgz#0a8279f06be37f9f0ebb567560a30a480da59a2e"
|
||||
integrity sha1-CoJ58Gvjf58Ou1Z1YKMKSA2lmi4=
|
||||
dependencies:
|
||||
biskviit "1.0.1"
|
||||
encoding "0.1.12"
|
||||
|
||||
figures@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
|
||||
|
@ -5674,6 +5723,13 @@ ip-regex@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd"
|
||||
integrity sha1-3FiQdvZZ9BnCIgOaMzFvHHOH7/0=
|
||||
|
||||
ip2country@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ip2country/-/ip2country-1.0.1.tgz#e2ab284b774b65c89509679fcb82552afcff9804"
|
||||
integrity sha512-wYhIyQzcP85tKo17HwitnHB7F3vbN+gA7DqZzeE5K1NLfr4XnKZQ1RNsMGm3bNhf1eA3bz9QFjSXo4q6VKRqCw==
|
||||
dependencies:
|
||||
asbycountry "^1.4.2"
|
||||
|
||||
ip@^1.1.0, ip@^1.1.5:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
|
||||
|
@ -8859,7 +8915,7 @@ pseudomap@^1.0.2:
|
|||
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
|
||||
integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
|
||||
|
||||
psl@^1.1.28:
|
||||
psl@^1.1.28, psl@^1.1.7:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
|
||||
integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
|
||||
|
@ -11854,6 +11910,13 @@ url@^0.11.0, url@~0.11.0:
|
|||
punycode "1.3.2"
|
||||
querystring "0.2.0"
|
||||
|
||||
use-hooks@^2.0.0-rc.5:
|
||||
version "2.0.0-rc.5"
|
||||
resolved "https://registry.yarnpkg.com/use-hooks/-/use-hooks-2.0.0-rc.5.tgz#05bdfb89cf088ef49961858b529d090a15f48680"
|
||||
integrity sha512-85OzZPao3Rk6GFJqaeTHRj5MluTFhzDxUikwytiwJYGR4+2tEeH86Ym1DMDM6NzJ3bwueaX7kc+rlF/5J074TA==
|
||||
dependencies:
|
||||
"@types/react" "^16.8.3"
|
||||
|
||||
use-strict@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/use-strict/-/use-strict-1.0.1.tgz#0bb80d94f49a4a05192b84a8c7d34e95f1a7e3a0"
|
||||
|
|
Loading…
Reference in a new issue