public chat invitations

pull/645/head
Maxim Shishmarev 5 years ago
parent 8cd8c8d4f9
commit a5fce17d8c

@ -2229,5 +2229,14 @@
},
"groupNamePlaceholder": {
"message": "Group Name"
},
"inviteFriends": {
"message": "Invite Friends"
},
"groupInvitation": {
"message": "Group Invitation"
},
"add-friends": {
"message": "Adding friends to"
}
}

@ -817,6 +817,7 @@
<script type='text/javascript' src='js/views/device_pairing_words_dialog_view.js'></script>
<script type='text/javascript' src='js/views/create_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/invite_friends_dialog_view.js'></script>
<script type='text/javascript' src='js/wall_clock_listener.js'></script>
<script type='text/javascript' src='js/rotate_signed_prekey_listener.js'></script>

@ -799,6 +799,22 @@
appView.openConversation(groupId, {});
};
window.sendGroupInvitations = (serverInfo, pubkeys) => {
pubkeys.forEach(async pubkey => {
const convo = await ConversationController.getOrCreateAndWait(
pubkey,
'private'
);
if (convo) {
convo.sendMessage('', null, null, null, {
serverName: serverInfo.name,
serverAddress: serverInfo.address,
});
}
});
};
Whisper.events.on('createNewGroup', async () => {
if (appView) {
appView.showCreateGroup();
@ -811,6 +827,50 @@
}
});
Whisper.events.on('inviteFriends', async groupConvo => {
if (appView) {
appView.showInviteFriendsDialog(groupConvo);
}
});
Whisper.events.on('invitationAccepted', async serverAddress => {
// To some degree this has been copy-pasted
// form connection_to_server_dialog_view.js:
const channelId = 1;
const rawServerUrl = serverAddress
.replace(/^https?:\/\//i, '')
.replace(/[/\\]+$/i, '');
const sslServerUrl = `https://${rawServerUrl}`;
const conversationId = `publicChat:${channelId}@${rawServerUrl}`;
const conversationExists = ConversationController.get(conversationId);
if (conversationExists) {
window.log.warn('We are already a member of this public chat');
return;
}
const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer(
sslServerUrl
);
if (!serverAPI) {
window.log.warn(`Could not connect to ${serverAddress}`);
return;
}
const conversation = await ConversationController.getOrCreateAndWait(
conversationId,
'group'
);
serverAPI.findOrCreateChannel(channelId, conversationId);
await conversation.setPublicSource(sslServerUrl, channelId);
await conversation.setFriendRequestStatus(
window.friends.friendRequestStatusEnum.friends
);
appView.openConversation(conversationId, {});
});
Whisper.events.on('leaveGroup', async groupConvo => {
if (appView) {
appView.showLeaveGroupDialog(groupConvo);

@ -1418,7 +1418,13 @@
};
},
async sendMessage(body, attachments, quote, preview) {
async sendMessage(
body,
attachments,
quote,
preview,
groupInvitation = null
) {
this.clearTypingTimers();
const destination = this.id;
@ -1510,6 +1516,7 @@
}
const attributes = {
...messageWithSchema,
group_invitation: groupInvitation,
id: window.getGuid(),
};
@ -1589,6 +1596,8 @@
options.publicSendData = await this.getPublicSendData();
}
options.groupInvitation = groupInvitation;
const groupNumbers = this.getRecipients();
const promise = (() => {

@ -104,6 +104,8 @@
this.propsForGroupNotification = this.getPropsForGroupNotification();
} else if (this.isFriendRequest()) {
this.propsForFriendRequest = this.getPropsForFriendRequest();
} else if (this.isGroupInvitation()) {
this.propsForGroupInvitation = this.getPropsForGroupInvitation();
} else {
this.propsForSearchResult = this.getPropsForSearchResult();
this.propsForMessage = this.getPropsForMessage();
@ -250,6 +252,9 @@
if (this.isIncoming() && this.hasErrors()) {
return i18n('incomingError');
}
if (this.isGroupInvitation()) {
return `<${i18n('groupInvitation')}>`;
}
return this.get('body');
},
isVerifiedChange() {
@ -261,6 +266,9 @@
isFriendRequest() {
return this.get('type') === 'friend-request';
},
isGroupInvitation() {
return !!this.get('group_invitation');
},
getNotificationText() {
const description = this.getDescription();
if (description) {
@ -438,6 +446,29 @@
onRetrySend,
};
},
getPropsForGroupInvitation() {
const invitation = this.get('group_invitation');
let direction = 'incoming';
if (this.get('direction')) {
direction = this.get('direction');
} else if (this.get('type') === 'outgoing') {
direction = 'outgoing';
}
return {
serverName: invitation.serverName,
serverAddress: invitation.serverAddress,
direction,
onClick: () => {
Whisper.events.trigger(
'invitationAccepted',
invitation.serverAddress
);
},
};
},
findContact(phoneNumber) {
return ConversationController.get(phoneNumber);
},
@ -1991,6 +2022,10 @@
}
}
if (initialMessage.groupInvitation) {
message.set({ group_invitation: initialMessage.groupInvitation });
}
const urls = window.Signal.LinkPreviews.findLinks(dataMessage.body);
const incomingPreview = dataMessage.preview || [];
const preview = incomingPreview.filter(
@ -2216,15 +2251,6 @@
} else if (message.get('type') !== 'outgoing') {
// Ignore 'outgoing' messages because they are sync messages
await sendingDeviceConversation.onFriendRequestAccepted();
// We need to return for these types of messages because android struggles
if (
!message.get('body') &&
!message.get('attachments').length &&
!message.get('preview').length &&
!message.get('group_update')
) {
return;
}
}
const id = await window.Signal.Data.saveMessage(message.attributes, {
Message: Whisper.Message,

@ -52,6 +52,12 @@ const { EditProfileDialog } = require('../../ts/components/EditProfileDialog');
const {
UpdateGroupDialog,
} = require('../../ts/components/conversation/UpdateGroupDialog');
const {
InviteFriendsDialog,
} = require('../../ts/components/conversation/InviteFriendsDialog');
const {
GroupInvitation,
} = require('../../ts/components/conversation/GroupInvitation');
const { ConfirmDialog } = require('../../ts/components/ConfirmDialog');
const {
MediaGallery,
@ -232,6 +238,8 @@ exports.setup = (options = {}) => {
EditProfileDialog,
ConfirmDialog,
UpdateGroupDialog,
InviteFriendsDialog,
GroupInvitation,
BulkEdit,
MediaGallery,
Message,

@ -251,5 +251,9 @@
const dialog = new Whisper.LeaveGroupDialogView(groupConvo);
this.el.append(dialog.el);
},
showInviteFriendsDialog(groupConvo) {
const dialog = new Whisper.InviteFriendsDialogView(groupConvo);
this.el.append(dialog.el);
},
});
})();

@ -286,6 +286,10 @@
onLeaveGroup: () => {
window.Whisper.events.trigger('leaveGroup', this.model);
},
onInviteFriends: () => {
window.Whisper.events.trigger('inviteFriends', this.model);
},
};
};
this.titleView = new Whisper.ReactWrapperView({

@ -0,0 +1,53 @@
/* global Whisper */
// eslint-disable-next-line func-names
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.InviteFriendsDialogView = Whisper.View.extend({
className: 'loki-dialog modal',
initialize(convo) {
this.close = this.close.bind(this);
this.submit = this.submit.bind(this);
const convos = window.getConversations().models;
const friends = convos.filter(
d => !!d && d.isFriend() && d.isPrivate() && !d.isMe()
);
this.friends = friends;
this.chatName = convo.get('name');
this.chatServer = convo.get('server');
this.$el.focus();
this.render();
},
render() {
const view = new Whisper.ReactWrapperView({
className: 'invite-friends-dialog',
Component: window.Signal.Components.InviteFriendsDialog,
props: {
friendList: this.friends,
onSubmit: this.submit,
onClose: this.close,
chatName: this.chatName,
},
});
this.$el.append(view.el);
return this;
},
close() {
this.remove();
},
submit(pubkeys) {
window.sendGroupInvitations(
{ address: this.chatServer, name: this.chatName },
pubkeys
);
},
});
})();

@ -74,6 +74,11 @@
Component: Components.FriendRequest,
props: this.model.propsForFriendRequest,
};
} else if (this.model.propsForGroupInvitation) {
return {
Component: Components.GroupInvitation,
props: this.model.propsForGroupInvitation,
};
}
return {

@ -27,6 +27,7 @@ function Message(options) {
this.expireTimer = options.expireTimer;
this.profileKey = options.profileKey;
this.profile = options.profile;
this.groupInvitation = options.groupInvitation;
if (!(this.recipients instanceof Array)) {
throw new Error('Invalid recipient list');
@ -160,6 +161,15 @@ Message.prototype = {
proto.profile = profile;
}
if (this.groupInvitation) {
proto.groupInvitation = new textsecure.protobuf.DataMessage.GroupInvitation(
{
serverAddress: this.groupInvitation.serverAddress,
serverName: this.groupInvitation.serverName,
}
);
}
this.dataMessage = proto;
return proto;
},
@ -404,7 +414,7 @@ MessageSender.prototype = {
);
numbers.forEach(number => {
// Note: if we are sending a private group message, we make our best to
// Note: if we are sending a private group message, we do our best to
// ensure we have signal protocol sessions with every member, but if we
// fail, let's at least send messages to those members with which we do:
const haveSession = _.some(
@ -941,6 +951,8 @@ MessageSender.prototype = {
? textsecure.protobuf.DataMessage.Flags.BACKGROUND_FRIEND_REQUEST
: undefined;
const { groupInvitation } = options;
return this.sendMessage(
{
recipients: [number],
@ -954,6 +966,7 @@ MessageSender.prototype = {
profileKey,
profile,
flags,
groupInvitation,
},
options
);

@ -200,6 +200,11 @@ message DataMessage {
optional string avatar = 2;
}
message GroupInvitation {
optional string serverAddress = 1;
optional string serverName = 2;
}
optional string body = 1;
repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3;
@ -211,6 +216,7 @@ message DataMessage {
repeated Contact contact = 9;
repeated Preview preview = 10;
optional LokiProfile profile = 101; // Loki: The profile of the current user
optional GroupInvitation groupInvitation = 102; // Loki: Invitation to a public chat
}
message NullMessage {

@ -204,6 +204,80 @@
padding-top: 4px;
}
.group-invitation-container {
display: flex;
flex-direction: column;
}
.group-invitation {
background-color: #f4f4f0;
display: inline-block;
margin: 4px 16px;
padding: 4px;
border: solid;
border-width: 0.5px;
border-radius: 4px;
border-color: #e0e0e0;
align-self: flex-start;
box-shadow: 2px 2px lightgrey;
.title {
margin: 6px;
color: darkslategray;
font-variant-caps: all-small-caps;
user-select: none;
}
.contents {
display: flex;
align-items: center;
margin: 6px;
.invite-group-avatar {
height: 48px;
width: 48px;
}
.group-details {
display: inline-flex;
flex-direction: column;
padding: 8px;
.group-name {
font-weight: lighter;
padding-bottom: 4px;
}
.group-address {
color: grey;
}
}
.join-btn {
background-color: #e0e0e0;
padding: 6px 10px;
margin-left: 6px;
border-radius: 6px;
box-shadow: 2px 2px 1px #c0c0c0;
color: #404040;
user-select: none;
cursor: pointer;
&:hover {
background-color: #c7c7c7;
}
}
}
}
.invitation-outgoing {
align-self: flex-end;
}
.message-selected {
background-color: #60554060;
}

@ -29,6 +29,7 @@
margin-left: 10px;
}
.invite-friends-dialog,
.create-group-dialog {
.content {
max-width: 100% !important;
@ -46,7 +47,9 @@
font-size: large;
text-align: center;
}
}
.create-group-dialog {
.no-friends {
text-align: center;
}
@ -124,7 +127,8 @@
}
.member-list-container,
.create-group-dialog {
.create-group-dialog,
.invite-friends-dialog {
.member-item {
padding: 4px;
user-select: none;
@ -176,7 +180,8 @@
.dark-theme {
.member-list-container,
.create-group-dialog {
.create-group-dialog,
.invite-friends-dialog {
.member-item {
&:hover:not(.member-selected) {
background-color: $color-dark-55;

@ -576,7 +576,7 @@
<script type='text/javascript' src='../js/views/banner_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/clear_data_view.js'></script>
<script type='text/javascript' src='../js/views/create_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/invite_friends_dialog_view.js'></script>
<script type='text/javascript' src='../js/views/beta_release_disclaimer_view.js'></script>

@ -42,6 +42,7 @@ export type PropsData = {
hasNickname?: boolean;
isFriendItem?: boolean;
isSecondary?: boolean;
isGroupInvitation?: boolean;
};
type PropsHousekeeping = {

@ -67,6 +67,8 @@ interface Props {
onUpdateGroup: () => void;
onLeaveGroup: () => void;
onInviteFriends: () => void;
i18n: LocalizerType;
}
@ -230,6 +232,7 @@ export class ConversationHeader extends React.Component<Props> {
onCopyPublicKey,
onUpdateGroup,
onLeaveGroup,
onInviteFriends,
} = this.props;
const isPrivateGroup = isGroup && !isPublic;
@ -248,6 +251,9 @@ export class ConversationHeader extends React.Component<Props> {
<MenuItem onClick={onLeaveGroup}>{i18n('leaveGroup')}</MenuItem>
) : null}
{/* TODO: add delete group */}
{isGroup && isPublic ? (
<MenuItem onClick={onInviteFriends}>{i18n('inviteFriends')}</MenuItem>
) : null}
{!isMe && isClosable && !isPrivateGroup ? (
!isPublic ? (
<MenuItem onClick={onDeleteContact}>

@ -0,0 +1,45 @@
import React from 'react';
import classNames from 'classnames';
interface Props {
serverName: string;
serverAddress: string;
direction: string;
onClick: any;
}
export class GroupInvitation extends React.Component<Props> {
public render() {
const classes = ['group-invitation'];
if (this.props.direction === 'outgoing') {
classes.push('invitation-outgoing');
}
return (
<div className={'group-invitation-container'}>
<div className={classNames(classes)}>
<div className="title">Group invitation</div>
<div className="contents">
<img
alt="group-avatar"
src="images/loki/loki_icon.png"
className="invite-group-avatar"
/>
<span className="group-details">
<span className="group-name">{this.props.serverName}</span>
<span className="group-address">{this.props.serverAddress}</span>
</span>
<span
role="button"
className="join-btn"
onClick={this.props.onClick}
>
Join
</span>
</div>
</div>
</div>
);
}
}

@ -0,0 +1,132 @@
import React from 'react';
import { Contact, MemberList } from './MemberList';
interface Props {
friendList: Array<any>;
chatName: string;
onSubmit: any;
onClose: any;
}
declare global {
interface Window {
i18n: any;
}
}
interface State {
friendList: Array<Contact>;
}
export class InviteFriendsDialog extends React.Component<Props, State> {
constructor(props: any) {
super(props);
this.onMemberClicked = this.onMemberClicked.bind(this);
this.closeDialog = this.closeDialog.bind(this);
this.onClickOK = this.onClickOK.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
let friends = this.props.friendList;
friends = friends.map(d => {
const lokiProfile = d.getLokiProfile();
const name = lokiProfile ? lokiProfile.displayName : 'Anonymous';
// TODO: should take existing members into account
const existingMember = false;
return {
id: d.id,
authorPhoneNumber: d.id,
authorProfileName: name,
selected: false,
authorName: name,
authorColor: d.getColor(),
checkmarked: false,
existingMember,
};
});
this.state = {
friendList: friends,
};
window.addEventListener('keyup', this.onKeyUp);
}
public render() {
const titleText = `${window.i18n('add-friends')} ${this.props.chatName}`;
const cancelText = window.i18n('cancel');
const okText = window.i18n('ok');
return (
<div className="content">
<p className="titleText">{titleText}</p>
<div className="friend-selection-list">
<MemberList
members={this.state.friendList}
selected={{}}
i18n={window.i18n}
onMemberClicked={this.onMemberClicked}
/>
</div>
<div className="buttons">
<button className="cancel" tabIndex={0} onClick={this.closeDialog}>
{cancelText}
</button>
<button className="ok" tabIndex={0} onClick={this.onClickOK}>
{okText}
</button>
</div>
</div>
);
}
private onClickOK() {
const selectedFriends = this.state.friendList
.filter(d => d.checkmarked)
.map(d => d.id);
if (selectedFriends.length > 0) {
this.props.onSubmit(selectedFriends);
}
this.closeDialog();
}
private onKeyUp(event: any) {
switch (event.key) {
case 'Enter':
this.onClickOK();
break;
case 'Esc':
case 'Escape':
this.closeDialog();
break;
default:
}
}
private closeDialog() {
window.removeEventListener('keyup', this.onKeyUp);
this.props.onClose();
}
private onMemberClicked(selected: any) {
const updatedFriends = this.state.friendList.map(member => {
if (member.id === selected.id) {
return { ...member, checkmarked: !member.checkmarked };
} else {
return member;
}
});
this.setState(state => {
return {
...state,
friendList: updatedFriends,
};
});
}
}
Loading…
Cancel
Save