From 6d5aed7de8cdb59db850e6bfc344c03604970a7a Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 19 Feb 2020 11:27:37 +1100 Subject: [PATCH] make upload of group picture work --- _locales/en/messages.json | 2 +- js/background.js | 48 ++++++++- js/modules/loki_app_dot_net_api.js | 34 +++++- js/views/conversation_view.js | 3 + js/views/create_group_dialog_view.js | 102 +++++++----------- js/views/invite_friends_dialog_view.js | 5 +- stylesheets/_index.scss | 1 + .../conversation/UpdateGroupMembersDialog.tsx | 12 --- .../conversation/UpdateGroupNameDialog.tsx | 86 +++++++++++++-- .../session/LeftPaneChannelSection.tsx | 24 +++-- .../session/SessionGroupSettings.tsx | 41 +++---- 11 files changed, 243 insertions(+), 115 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 5ee80af32..03ff620bb 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2205,7 +2205,7 @@ "description": "Button action that the user can click to edit their profile" }, "editGroupName": { - "message": "Edit group name", + "message": "Edit group name or picture", "description": "Button action that the user can click to edit a group name" }, "createGroupDialogTitle": { diff --git a/js/background.js b/js/background.js index 859909b61..5478fbffc 100644 --- a/js/background.js +++ b/js/background.js @@ -702,7 +702,7 @@ } }); - window.doUpdateGroup = async (groupId, groupName, members) => { + window.doUpdateGroup = async (groupId, groupName, members, avatar) => { const ourKey = textsecure.storage.user.getNumber(); const ev = new Event('message'); @@ -729,6 +729,44 @@ if (convo.isPublic()) { const API = await convo.getPublicSendData(); + + if (avatar) { + // I hate duplicating this... + 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 attachment = await readFile({ file: avatar }); + // const tempUrl = window.URL.createObjectURL(avatar); + + // Get file onto public chat server + const fileObj = await API.serverAPI.putAttachment(attachment.data); + if (fileObj === null) { + // problem + log.warn('File upload failed'); + return; + } + + // lets not allow ANY URLs, lets force it to be local to public chat server + const relativeFileUrl = fileObj.url.replace( + API.serverAPI.baseServerUrl, + '' + ); + // write it to the channel + const changeRes = await API.setChannelAvatar(relativeFileUrl); + } + if (await API.setChannelName(groupName)) { // queue update from server // and let that set the conversation @@ -741,7 +779,11 @@ return; } - const avatar = ''; + const nullAvatar = ''; + if (avatar) { + // would get to download this file on each client in the group + // and reference the local file + } const options = {}; const recipients = _.union(convo.get('members'), members); @@ -750,7 +792,7 @@ convo.updateGroup({ groupId, groupName, - avatar, + nullAvatar, recipients, members, options, diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index c9664abef..ddb201663 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -877,6 +877,7 @@ class LokiAppDotNetServerAPI { }; } + // for avatar async uploadData(data) { const endpoint = 'files'; const options = { @@ -901,6 +902,7 @@ class LokiAppDotNetServerAPI { }; } + // for files putAttachment(attachmentBin) { const formData = new FormData(); const buffer = Buffer.from(attachmentBin); @@ -1246,7 +1248,37 @@ class LokiPublicChannelAPI { this.conversation.setGroupName(note.value.name); } if (note.value && note.value.avatar) { - this.conversation.setProfileAvatar(note.value.avatar); + const avatarAbsUrl = this.serverAPI.baseServerUrl + note.value.avatar; + console.log('setting', avatarAbsUrl); + const { + upgradeMessageSchema, + writeNewAttachmentData, + deleteAttachmentData, + } = window.Signal.Migrations; + // do we already have this image? no, then + + // download a copy and save it + const imageData = await nodeFetch(avatarAbsUrl); + function toArrayBuffer(buf) { + var ab = new ArrayBuffer(buf.length); + var view = new Uint8Array(ab); + for (var i = 0; i < buf.length; ++i) { + view[i] = buf[i]; + } + return ab; + } + const newAttributes = await window.Signal.Types.Conversation.maybeUpdateAvatar( + this.conversation.attributes, + toArrayBuffer(imageData), + { + writeNewAttachmentData, + deleteAttachmentData, + } + ); + console.log('newAttributes.avatar', newAttributes.avatar); + // update group + this.conversation.set(newAttributes); + //this.conversation.setProfileAvatar(newAttributes.avatar); } // is it mutable? // who are the moderators? diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 0550fa63e..9f28d86c7 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -287,6 +287,9 @@ isAdmin: this.model.get('groupAdmins').includes(ourPK), isRss: this.model.isRss(), memberCount: members.length, + amMod: this.model.isModerator( + window.storage.get('primaryDevicePubKey') + ), timerOptions: Whisper.ExpirationTimerOptions.map(item => ({ name: item.getName(), diff --git a/js/views/create_group_dialog_view.js b/js/views/create_group_dialog_view.js index befbf3b3b..193fd1ed0 100644 --- a/js/views/create_group_dialog_view.js +++ b/js/views/create_group_dialog_view.js @@ -59,31 +59,12 @@ this.close = this.close.bind(this); this.onSubmit = this.onSubmit.bind(this); this.isPublic = groupConvo.isPublic(); + this.groupId = groupConvo.id; const ourPK = textsecure.storage.user.getNumber(); this.isAdmin = groupConvo.get('groupAdmins').includes(ourPK); - const convos = window.getConversations().models.filter(d => !!d); - - let existingMembers = groupConvo.get('members') || []; - - // Show a contact if they are our friend or if they are a member - const friendsAndMembers = convos.filter( - d => - (d.isFriend() || existingMembers.includes(d.id)) && - d.isPrivate() && - !d.isMe() - ); - this.friendsAndMembers = _.uniq(friendsAndMembers, true, d => d.id); - - // at least make sure it's an array - if (!Array.isArray(existingMembers)) { - existingMembers = []; - } - - this.existingMembers = existingMembers; - // public chat settings overrides if (this.isPublic) { // fix the title @@ -98,6 +79,24 @@ // zero out friendList for now this.friendsAndMembers = []; this.existingMembers = []; + } else { + const convos = window.getConversations().models.filter(d => !!d); + + this.existingMembers = groupConvo.get('members') || []; + // Show a contact if they are our friend or if they are a member + this.friendsAndMembers = convos.filter( + d => this.existingMembers.includes(d.id) && d.isPrivate() && !d.isMe() + ); + this.friendsAndMembers = _.uniq( + this.friendsAndMembers, + true, + d => d.id + ); + + // at least make sure it's an array + if (!Array.isArray(this.existingMembers)) { + this.existingMembers = []; + } } this.$el.focus(); @@ -109,24 +108,22 @@ Component: window.Signal.Components.UpdateGroupNameDialog, props: { titleText: this.titleText, - groupName: this.groupName, - okText: this.okText, isPublic: this.isPublic, - cancelText: this.cancelText, - existingMembers: this.existingMembers, + groupName: this.groupName, + okText: i18n('ok'), + cancelText: i18n('cancel'), isAdmin: this.isAdmin, - onClose: this.close, + i18n, onSubmit: this.onSubmit, + onClose: this.close, }, }); this.$el.append(this.dialogView.el); return this; }, - onSubmit(newGroupName, members) { - const groupId = this.conversation.get('id'); - - window.doUpdateGroup(groupId, newGroupName, members); + onSubmit(groupName, avatar) { + window.doUpdateGroup(this.groupId, groupName, this.members, avatar); }, close() { this.remove(); @@ -136,40 +133,16 @@ Whisper.UpdateGroupMembersDialogView = Whisper.View.extend({ className: 'loki-dialog modal', initialize(groupConvo) { + const ourPK = textsecure.storage.user.getNumber(); this.groupName = groupConvo.get('name'); - - this.conversation = groupConvo; - this.titleText = i18n('updateGroupDialogTitle'); - this.okText = i18n('ok'); - this.cancelText = i18n('cancel'); this.close = this.close.bind(this); this.onSubmit = this.onSubmit.bind(this); this.isPublic = groupConvo.isPublic(); + this.groupId = groupConvo.id; + this.avatarPath = groupConvo.getAvatarPath(); + this.members = groupConvo.get('members') || []; - const ourPK = textsecure.storage.user.getNumber(); - - this.isAdmin = groupConvo.get('groupAdmins').includes(ourPK); - - const convos = window.getConversations().models.filter(d => !!d); - - let existingMembers = groupConvo.get('members') || []; - - // Show a contact if they are our friend or if they are a member - const friendsAndMembers = convos.filter( - d => existingMembers.includes(d.id) && d.isPrivate() && !d.isMe() - ); - this.friendsAndMembers = _.uniq(friendsAndMembers, true, d => d.id); - - // at least make sure it's an array - if (!Array.isArray(existingMembers)) { - existingMembers = []; - } - - this.existingMembers = existingMembers; - - // public chat settings overrides if (this.isPublic) { - // fix the title this.titleText = `${i18n('updatePublicGroupDialogTitle')}: ${ this.groupName }`; @@ -178,9 +151,9 @@ this.isAdmin = groupConvo.isModerator( window.storage.get('primaryDevicePubKey') ); - // zero out friendList for now - this.friendsAndMembers = []; - this.existingMembers = []; + } else { + this.titleText = i18n('updateGroupDialogTitle'); + this.isAdmin = groupConvo.get('groupAdmins').includes(ourPK); } this.$el.focus(); @@ -201,6 +174,7 @@ isAdmin: this.isAdmin, onClose: this.close, onSubmit: this.onSubmit, + groupId: this.groupId, }, }); @@ -210,9 +184,13 @@ onSubmit(groupName, newMembers) { const ourPK = textsecure.storage.user.getNumber(); const allMembers = window.Lodash.concat(newMembers, [ourPK]); - const groupId = this.conversation.get('id'); - window.doUpdateGroup(groupId, groupName, allMembers); + window.doUpdateGroup( + this.groupId, + groupName, + allMembers, + this.avatarPath + ); }, close() { this.remove(); diff --git a/js/views/invite_friends_dialog_view.js b/js/views/invite_friends_dialog_view.js index 1cc7c0ec0..ddf8d5745 100644 --- a/js/views/invite_friends_dialog_view.js +++ b/js/views/invite_friends_dialog_view.js @@ -74,7 +74,10 @@ newMembers.length + existingMembers.length > window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT ) { - const msg = window.i18n('maxGroupMembersError', window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT); + const msg = window.i18n( + 'maxGroupMembersError', + window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT + ); window.pushToast({ title: msg, diff --git a/stylesheets/_index.scss b/stylesheets/_index.scss index f5156f5dc..672319a6a 100644 --- a/stylesheets/_index.scss +++ b/stylesheets/_index.scss @@ -11,6 +11,7 @@ } .edit-profile-dialog, +.create-group-dialog, .user-details-dialog { .content { max-width: 100% !important; diff --git a/ts/components/conversation/UpdateGroupMembersDialog.tsx b/ts/components/conversation/UpdateGroupMembersDialog.tsx index 27de62c49..7c3f77775 100644 --- a/ts/components/conversation/UpdateGroupMembersDialog.tsx +++ b/ts/components/conversation/UpdateGroupMembersDialog.tsx @@ -34,7 +34,6 @@ export class UpdateGroupMembersDialog extends React.Component { this.onClickOK = this.onClickOK.bind(this); this.onKeyUp = this.onKeyUp.bind(this); this.closeDialog = this.closeDialog.bind(this); - this.onGroupNameChanged = this.onGroupNameChanged.bind(this); let friends = this.props.friendList; friends = friends.map(d => { @@ -209,15 +208,4 @@ export class UpdateGroupMembersDialog extends React.Component { }; }); } - - private onGroupNameChanged(event: any) { - event.persist(); - - this.setState(state => { - return { - ...state, - groupName: event.target.value, - }; - }); - } } diff --git a/ts/components/conversation/UpdateGroupNameDialog.tsx b/ts/components/conversation/UpdateGroupNameDialog.tsx index ba44a2497..bc4a1a95d 100644 --- a/ts/components/conversation/UpdateGroupNameDialog.tsx +++ b/ts/components/conversation/UpdateGroupNameDialog.tsx @@ -3,9 +3,11 @@ import classNames from 'classnames'; import { SessionModal } from '../session/SessionModal'; import { SessionButton } from '../session/SessionButton'; +import { Avatar } from '../Avatar'; interface Props { titleText: string; + isPublic: boolean; groupName: string; okText: string; cancelText: string; @@ -13,30 +15,35 @@ interface Props { i18n: any; onSubmit: any; onClose: any; - existingMembers: Array; + // avatar stuff + avatarPath: string; } interface State { groupName: string; errorDisplayed: boolean; errorMessage: string; + avatar: string; } export class UpdateGroupNameDialog extends React.Component { + private readonly inputEl: any; + constructor(props: any) { super(props); this.onClickOK = this.onClickOK.bind(this); this.onKeyUp = this.onKeyUp.bind(this); this.closeDialog = this.closeDialog.bind(this); - this.onGroupNameChanged = this.onGroupNameChanged.bind(this); + this.onFileSelected = this.onFileSelected.bind(this); this.state = { groupName: this.props.groupName, errorDisplayed: false, errorMessage: 'placeholder', + avatar: this.props.avatarPath, }; - + this.inputEl = React.createRef(); window.addEventListener('keyup', this.onKeyUp); } @@ -47,18 +54,30 @@ export class UpdateGroupNameDialog extends React.Component { return; } - this.props.onSubmit(this.state.groupName, this.props.existingMembers); + const avatar = + this.inputEl && + this.inputEl.current && + this.inputEl.current.files && + this.inputEl.current.files.length > 0 + ? this.inputEl.current.files[0] + : this.props.avatarPath; // otherwise use the current avatar + + this.props.onSubmit(this.props.groupName, avatar); this.closeDialog(); } public render() { - const okText = this.props.okText; - const cancelText = this.props.cancelText; + const { isPublic, okText, cancelText } = this.props; - let titleText; + const titleText = `${this.props.titleText}`; + let noAvatarClasses; - titleText = `${this.props.titleText}`; + if (isPublic) { + noAvatarClasses = classNames('avatar-center'); + } else { + noAvatarClasses = classNames('hidden'); + } const errorMsg = this.state.errorMessage; const errorMessageClasses = classNames( @@ -77,6 +96,33 @@ export class UpdateGroupNameDialog extends React.Component {

{errorMsg}

+
+
+ {this.renderAvatar()} +
+ +
{ + const el = this.inputEl.current; + if (el) { + el.click(); + } + }} + /> +
+
+
+
+ { }; }); } + + private renderAvatar() { + const avatarPath = this.state.avatar; + const color = '#00ff00'; + + return ( + + ); + } + + private onFileSelected() { + const file = this.inputEl.current.files[0]; + const url = window.URL.createObjectURL(file); + + this.setState({ + avatar: url, + }); + } } diff --git a/ts/components/session/LeftPaneChannelSection.tsx b/ts/components/session/LeftPaneChannelSection.tsx index 569d9bec1..fd543ebef 100644 --- a/ts/components/session/LeftPaneChannelSection.tsx +++ b/ts/components/session/LeftPaneChannelSection.tsx @@ -399,13 +399,18 @@ export class LeftPaneChannelSection extends React.Component { groupMembers: Array ) { // Validate groupName and groupMembers length - if (groupName.length === 0 || - groupName.length > window.CONSTANTS.MAX_GROUP_NAME_LENGTH) { - window.pushToast({ - title: window.i18n('invalidGroupName', window.CONSTANTS.MAX_GROUP_NAME_LENGTH), - type: 'error', - id: 'invalidGroupName', - }); + if ( + groupName.length === 0 || + groupName.length > window.CONSTANTS.MAX_GROUP_NAME_LENGTH + ) { + window.pushToast({ + title: window.i18n( + 'invalidGroupName', + window.CONSTANTS.MAX_GROUP_NAME_LENGTH + ), + type: 'error', + id: 'invalidGroupName', + }); return; } @@ -416,7 +421,10 @@ export class LeftPaneChannelSection extends React.Component { groupMembers.length >= window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT ) { window.pushToast({ - title: window.i18n('invalidGroupSize', window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT), + title: window.i18n( + 'invalidGroupSize', + window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT + ), type: 'error', id: 'invalidGroupSize', }); diff --git a/ts/components/session/SessionGroupSettings.tsx b/ts/components/session/SessionGroupSettings.tsx index 8f831c577..cff0c7245 100644 --- a/ts/components/session/SessionGroupSettings.tsx +++ b/ts/components/session/SessionGroupSettings.tsx @@ -20,6 +20,7 @@ interface Props { timerOptions: Array; isPublic: boolean; isAdmin: boolean; + amMod: boolean; onGoBack: () => void; onInviteFriends: () => void; @@ -211,6 +212,7 @@ export class SessionGroupSettings extends React.Component { onLeaveGroup, isPublic, isAdmin, + amMod, } = this.props; const { documents, media, onItemClick } = this.state; const showMemberCount = !!(memberCount && memberCount > 0); @@ -228,6 +230,9 @@ export class SessionGroupSettings extends React.Component { }; }); + const showUpdateGroupNameButton = isPublic ? amMod : isAdmin; + const showUpdateGroupMembersButton = !isPublic && isAdmin; + return (
{this.renderHeader()} @@ -245,25 +250,23 @@ export class SessionGroupSettings extends React.Component { className="description" placeholder={window.i18n('description')} /> - {!isPublic && ( - <> - {isAdmin && ( -
- {window.i18n('editGroupName')} -
- )} -
- {window.i18n('showMembers')} -
- + {showUpdateGroupNameButton && ( +
+ {window.i18n('editGroupName')} +
+ )} + {showUpdateGroupMembersButton && ( +
+ {window.i18n('showMembers')} +
)} {/*
{window.i18n('notifications')}