Merge remote-tracking branch 'w/onion-paths' into clearnet

This commit is contained in:
Audric Ackermann 2021-06-17 14:55:25 +10:00
commit c9d7f4a1ab
No known key found for this signature in database
GPG key ID: 999F434D76324AD4
69 changed files with 4508 additions and 1282 deletions

File diff suppressed because it is too large Load diff

View file

@ -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 -->

View file

@ -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>

View file

@ -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) {

View file

@ -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);
// },
});
})();

View file

@ -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();
},
});
})();

View file

@ -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();
},
});
})();

View file

@ -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();
},
});
})();

View file

@ -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();
},
});
})();

View file

@ -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();
},
});
})();

View file

@ -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();
}
},
});
})();

View file

@ -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": {

View file

@ -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 {

View file

@ -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%;
// }
}
}

View file

@ -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%;

View file

@ -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 */
}

View file

@ -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,

View file

@ -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>

View file

@ -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 }));
}
}
}

View file

@ -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>
</>
);
};

View file

@ -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) {

View 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>
);
};

View file

@ -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();
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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 && (

View file

@ -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>
);
};

View file

@ -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>
);
}

View file

@ -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>) {

View file

@ -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>
);
}

View file

@ -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>
</>
);
};

View file

@ -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">

View file

@ -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>
);

View file

@ -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';

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
}

View file

@ -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>
)}
</>
);

View file

@ -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;
}

View 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>
);
};

View file

@ -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();

View file

@ -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} />;
}

View file

@ -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 {

View file

@ -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',

View file

@ -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}

View file

@ -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>
</>
);
};

View file

@ -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>
</>
);
};

View file

@ -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;
}

View file

@ -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>
)}

View file

@ -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);

View file

@ -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
View file

@ -0,0 +1 @@
declare module 'ip2country';

View file

@ -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() {

View file

@ -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;

View file

@ -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;
}

View file

@ -52,6 +52,9 @@ export const UI = {
WHITE_PALE: '#AFAFAF',
GREEN: '#00F782',
// CAUTION
WARNING: '#FFC02E',
// SEMANTIC COLORS
DANGER: '#FF453A',
DANGER_ALT: '#FF4538',

View file

@ -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

View file

@ -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)

View file

@ -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
),

View file

@ -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',

View file

@ -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 = {

View 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
View 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;

View file

@ -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

View file

@ -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
View file

@ -26,6 +26,7 @@ declare module 'styled-components' {
colors: {
accent: string;
accentButton: string;
warning: string;
destructive: string;
cellBackground: string;
modalBackground: string;

View file

@ -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
View file

@ -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;
}
}

View file

@ -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"