diff --git a/toxygen/app.py b/toxygen/app.py index 311ebe9..b76a047 100644 --- a/toxygen/app.py +++ b/toxygen/app.py @@ -30,6 +30,7 @@ from messenger.messenger import Messenger from network.tox_dns import ToxDns from history.history import History from file_transfers.file_transfers_messages_service import FileTransfersMessagesService +from groups.groups_service import GroupsService import styles.style # TODO: dynamic loading @@ -41,7 +42,7 @@ class App: self._tox = self._ms = self._init = self._main_loop = self._av_loop = None self._uri = self._toxes = self._tray = self._file_transfer_handler = self._contacts_provider = None self._friend_factory = self._calls_manager = self._contacts_manager = self._smiley_loader = self._tox_dns = None - self._group_factory = None + self._group_factory = self._groups_service = None if uri is not None and uri.startswith('tox:'): self._uri = uri[4:] self._path = path_to_profile @@ -322,9 +323,10 @@ class App: self._file_transfer_handler = FileTransfersHandler(self._tox, self._settings, self._contacts_provider, file_transfers_message_service, profile) messages_items_factory.set_file_transfers_handler(self._file_transfer_handler) + self._groups_service = GroupsService(self._tox, self._contacts_manager, self._contacts_provider) widgets_factory = WidgetsFactory(self._settings, profile, self._profile_manager, self._contacts_manager, self._file_transfer_handler, self._smiley_loader, self._plugin_loader, - self._toxes, self._version) + self._toxes, self._version, self._groups_service) self._tray = tray.init_tray(profile, self._settings, self._ms) self._ms.set_dependencies(widgets_factory, self._tray, self._contacts_manager, self._messenger, profile, self._plugin_loader, self._file_transfer_handler, history, self._calls_manager) @@ -335,7 +337,7 @@ class App: # callbacks initialization callbacks.init_callbacks(self._tox, profile, self._settings, self._plugin_loader, self._contacts_manager, self._calls_manager, self._file_transfer_handler, self._ms, self._tray, - self._messenger) + self._messenger, self._groups_service, self._contacts_provider) def _try_to_update(self): updating = updater.start_update_if_needed(self._version, self._settings) diff --git a/toxygen/contacts/contact_provider.py b/toxygen/contacts/contact_provider.py index 6d4e6b8..6f13596 100644 --- a/toxygen/contacts/contact_provider.py +++ b/toxygen/contacts/contact_provider.py @@ -38,7 +38,7 @@ class ContactProvider(tox_save.ToxSave): # ----------------------------------------------------------------------------------------------------------------- def get_all_groups(self): - group_numbers = range(self._tox.group_get_number_groups) + group_numbers = range(self._tox.group_get_number_groups()) groups = map(lambda n: self.get_group_by_number(n), group_numbers) return list(groups) diff --git a/toxygen/contacts/contacts_manager.py b/toxygen/contacts/contacts_manager.py index efca671..7dad02f 100644 --- a/toxygen/contacts/contacts_manager.py +++ b/toxygen/contacts/contacts_manager.py @@ -43,6 +43,12 @@ class ContactsManager: return self.get_curr_contact().number == friend_number + def is_group_active(self, group_number): + if self.is_active_a_friend(): + return False + + return self.get_curr_contact().number == friend_number + # ----------------------------------------------------------------------------------------------------------------- # Work with active friend # ----------------------------------------------------------------------------------------------------------------- @@ -299,7 +305,7 @@ class ContactsManager: friend.reset_avatar() def add_group(self, group_number): - group = self._contact_provider.get_group_by_numner(group_number) + group = self._contact_provider.get_group_by_number(group_number) self._contacts.append(group) group.reset_avatar() diff --git a/toxygen/contacts/group_chat.py b/toxygen/contacts/group_chat.py index c069ca4..d37bbe7 100644 --- a/toxygen/contacts/group_chat.py +++ b/toxygen/contacts/group_chat.py @@ -1,31 +1,21 @@ from contacts import contact import utils.util as util -from PyQt5 import QtGui, QtCore +from wrapper.toxcore_enums_and_consts import * from wrapper import toxcore_enums_and_consts as constants -# TODO: ngc - class GroupChat(contact.Contact): def __init__(self, profile_manager, name, status_message, widget, tox, group_number): super().__init__(None, group_number, profile_manager, name, status_message, widget, None) self._tox = tox self.set_status(constants.TOX_USER_STATUS['NONE']) + self._peers = [] + self._add_self_to_gc() - def set_name(self, name): - self._tox.group_set_title(self._number, name) - super().set_name(name) - - def send_message(self, message): - self._tox.group_message_send(self._number, message.encode('utf-8')) - - def new_title(self, title): - super().set_name(title) - - @staticmethod - def _get_default_avatar_path(): - return util.join_path(util.get_images_directory(), 'group.png') + def set_topic(self, topic): + self._tox.group_set_topic(self._number, topic.encode('utf-8')) + super().set_status_message(topic) def remove_invalid_unsent_files(self): pass @@ -45,3 +35,17 @@ class GroupChat(contact.Contact): def get_peer_name(self, peer_number): return self._tox.group_peername(self._number, peer_number) + + def get_self_name(self): + return self._peers[0].name + + # ----------------------------------------------------------------------------------------------------------------- + # Private methods + # ----------------------------------------------------------------------------------------------------------------- + + @staticmethod + def _get_default_avatar_path(): + return util.join_path(util.get_images_directory(), 'group.png') + + def _add_self_to_gc(self): + pass diff --git a/toxygen/contacts/group_peer_contact.py b/toxygen/contacts/group_peer_contact.py new file mode 100644 index 0000000..1c51f67 --- /dev/null +++ b/toxygen/contacts/group_peer_contact.py @@ -0,0 +1,13 @@ +import contacts.contact + + +class GroupPeerContact(contacts.contact.Contact): + + def __init__(self, profile_manager, message_getter, peer_number, name, status_messsage, widget, tox_id, group_pk): + super().__init__(profile_manager, message_getter, peer_number, name, status_messsage, widget, tox_id) + self._group_pk = group_pk + + def get_group_pk(self): + return self._group_pk + + group_pk = property(get_group_pk) diff --git a/toxygen/groups/group_peer.py b/toxygen/groups/group_peer.py new file mode 100644 index 0000000..f91decc --- /dev/null +++ b/toxygen/groups/group_peer.py @@ -0,0 +1,10 @@ + + +class GroupChatPeer: + + def __init__(self, peer_number, name, status, role, public_key): + self.peer_number = peer_number + self.name = name + self.status = status + self.role = role + self.public_key = public_key diff --git a/toxygen/messenger/messages.py b/toxygen/messenger/messages.py index c356485..91a1a04 100644 --- a/toxygen/messenger/messages.py +++ b/toxygen/messenger/messages.py @@ -106,7 +106,7 @@ class TextMessage(Message): class OutgoingTextMessage(TextMessage): - def __init__(self, message, owner, time, message_type, tox_message_id): + def __init__(self, message, owner, time, message_type, tox_message_id=0): super().__init__(message, owner, time, message_type) self._tox_message_id = tox_message_id diff --git a/toxygen/messenger/messenger.py b/toxygen/messenger/messenger.py index 6042db5..0a321f8 100644 --- a/toxygen/messenger/messenger.py +++ b/toxygen/messenger/messenger.py @@ -28,7 +28,7 @@ class Messenger(tox_save.ToxSave): self._items_factory.create_message_item(text_message) # ----------------------------------------------------------------------------------------------------------------- - # Messaging + # Messaging - friends # ----------------------------------------------------------------------------------------------------------------- def new_message(self, friend_number, message_type, message): @@ -53,7 +53,11 @@ class Messenger(tox_save.ToxSave): self._contacts_manager.update_filtration() def send_message(self): - self.send_message_to_friend(self._screen.messageEdit.toPlainText()) + text = self._screen.messageEdit.toPlainText() + if self._contacts_manager.is_active_a_friend(): + self.send_message_to_friend(text) + else: + self.send_message_to_group(text) def send_message_to_friend(self, text, friend_number=None): """ @@ -78,7 +82,6 @@ class Messenger(tox_save.ToxSave): for message in messages: if friend.status is not None: message_id = self._tox.friend_send_message(friend_number, message_type, message) - friend.inc_receipts() else: message_id = 0 message_author = MessageAuthor(self._profile.name, MESSAGE_AUTHOR['NOT_SENT']) @@ -103,6 +106,35 @@ class Messenger(tox_save.ToxSave): except Exception as ex: util.log('Sending pending messages failed with ' + str(ex)) + # ----------------------------------------------------------------------------------------------------------------- + # Messaging - groups + # ----------------------------------------------------------------------------------------------------------------- + + def send_message_to_group(self, text, group_number=None): + if group_number is None: + group_number = self._contacts_manager.get_active_number() + if text.startswith('/plugin '): + self._plugin_loader.command(text[8:]) + self._screen.messageEdit.clear() + elif text and group_number >= 0: + if text.startswith('/me '): + message_type = TOX_MESSAGE_TYPE['ACTION'] + text = text[4:] + else: + message_type = TOX_MESSAGE_TYPE['NORMAL'] + group = self._get_group_by_number(group_number) + messages = self._split_message(text.encode('utf-8')) + t = util.get_unix_time() + for message in messages: + self._tox.group_send_message(group_number, message_type, message) + message_author = MessageAuthor(group.get_self_name(), MESSAGE_AUTHOR['GC_PEER']) + message = OutgoingTextMessage(text, message_author, t, message_type) + group.append_message(message) + if self._contacts_manager.is_group_active(group_number): + self._create_message_item(message) + self._screen.messageEdit.clear() + self._screen.messages.scrollToBottom() + # ----------------------------------------------------------------------------------------------------------------- # Message receipts # ----------------------------------------------------------------------------------------------------------------- @@ -162,6 +194,9 @@ class Messenger(tox_save.ToxSave): def _get_friend_by_number(self, friend_number): return self._contacts_provider.get_friend_by_number(friend_number) + def _get_group_by_number(self, group_number): + return self._contacts_provider.get_group_by_number(group_number) + def _on_profile_name_changed(self, new_name): if self._profile_name == new_name: return diff --git a/toxygen/middleware/callbacks.py b/toxygen/middleware/callbacks.py index bd122d6..40b3284 100644 --- a/toxygen/middleware/callbacks.py +++ b/toxygen/middleware/callbacks.py @@ -13,7 +13,7 @@ from notifications.tray import tray_notification from notifications.sound import * import threading -# TODO: gc callbacks and refactoring. Use contact provider instead of manager +# TODO: refactoring. Use contact provider instead of manager # ----------------------------------------------------------------------------------------------------------------- # Callbacks - current user @@ -360,18 +360,49 @@ def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, u # ----------------------------------------------------------------------------------------------------------------- -def show_gc_notification(window, tray, message, group_number, peer_number): - profile = Profile.get_instance() - settings = Settings.get_instance() - chat = profile.get_group_by_number(group_number) - peer_name = chat.get_peer_name(peer_number) - if not window.isActiveWindow() and (profile.name in message or settings['group_notifications']): - if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked: - invoke_in_main_thread(tray_notification, chat.name + ' ' + peer_name, message, tray, window) - if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']: - sound_notification(SOUND_NOTIFICATION['MESSAGE']) - icon = os.path.join(util.get_images_directory(), 'icon_new_messages.png') - invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) +def group_message(window, tray, tox, messenger, settings, profile): + """ + New message in group chat + """ + def wrapped(tox_link, group_number, peer_id, message_type, message, length, user_data): + message = str(message[:length], 'utf-8') + invoke_in_main_thread(messenger.new_group_message, group_number, message_type, message, peer_id) + if not window.isActiveWindow(): + bl = settings['notify_all_gc'] or profile.name in message + name = tox.group_peer_get_name(group_number, peer_id) + if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and (not settings.locked) and bl: + invoke_in_main_thread(tray_notification, name, message, tray, window) + if settings['sound_notifications'] and bl and profile.status != TOX_USER_STATUS['BUSY']: + sound_notification(SOUND_NOTIFICATION['MESSAGE']) + icon = os.path.join(util.get_images_directory(), 'icon_new_messages.png') + invoke_in_main_thread(tray.setIcon, QtGui.QIcon(icon)) + return wrapped + + +def group_invite(groups_service): + def wrapped(tox, friend_number, invite_data, length, user_data): + invoke_in_main_thread(groups_service.process_group_invite, + friend_number, + bytes(invite_data[:length])) + + return wrapped + + +def group_self_join(contacts_provider): + def wrapped(tox, group_number, user_data): + group = contacts_provider.get_group_by_number(group_number) + invoke_in_main_thread(group.set_status, TOX_USER_STATUS['NONE']) + + return wrapped + + +def group_peer_join(contacts_provider): + def wrapped(tox, group_number, peer_id, user_data): + gc = contacts_provider.get_group_by_number(group_number) + gc.add_peer(peer_id) + + return wrapped + # ----------------------------------------------------------------------------------------------------------------- # Callbacks - initialization @@ -379,7 +410,8 @@ def show_gc_notification(window, tray, message, group_number, peer_number): def init_callbacks(tox, profile, settings, plugin_loader, contacts_manager, - calls_manager, file_transfer_handler, main_window, tray, messenger): + calls_manager, file_transfer_handler, main_window, tray, messenger, groups_service, + contacts_provider): """ Initialization of all callbacks. :param tox: Tox instance @@ -425,3 +457,9 @@ def init_callbacks(tox, profile, settings, plugin_loader, contacts_manager, # custom packets tox.callback_friend_lossless_packet(lossless_packet(plugin_loader), 0) tox.callback_friend_lossy_packet(lossy_packet(plugin_loader), 0) + + # gc callbacks + tox.callback_group_message(group_message(main_window, tray, tox, messenger, settings, profile), 0) + tox.callback_group_invite(group_invite(groups_service), 0) + tox.callback_group_self_join(group_self_join(contacts_provider), 0) + tox.callback_group_peer_join(group_peer_join(contacts_provider), 0) diff --git a/toxygen/ui/create_profile_screen.py b/toxygen/ui/create_profile_screen.py index 40c9091..08b85cf 100644 --- a/toxygen/ui/create_profile_screen.py +++ b/toxygen/ui/create_profile_screen.py @@ -26,9 +26,9 @@ class CreateProfileScreen(CenteredWidget, DialogWithResult): def __init__(self): CenteredWidget.__init__(self) DialogWithResult.__init__(self) - uic.loadUi(util.get_views_path('create_profile_screen')) + uic.loadUi(util.get_views_path('create_profile_screen'), self) self.center() - self.createProfile.clicked.connect(self.create_profile) + self.createProfile.clicked.connect(self._create_profile) def retranslateUi(self): self.defaultFolder.setPlaceholderText(util_ui.tr('Save in default folder')) @@ -36,7 +36,7 @@ class CreateProfileScreen(CenteredWidget, DialogWithResult): self.createProfile.setText(util_ui.tr('Create profile')) self.passwordLabel.setText(util_ui.tr('Password:')) - def create_profile(self): + def _create_profile(self): if self.password.text() != self.confirmPassword.text(): return # TODO : error result = CreateProfileScreenResult(self.defaultFolder.isChecked(), self.password.text()) diff --git a/toxygen/ui/groups_widgets.py b/toxygen/ui/groups_widgets.py new file mode 100644 index 0000000..4dc39d5 --- /dev/null +++ b/toxygen/ui/groups_widgets.py @@ -0,0 +1,72 @@ +from PyQt5 import uic +import utils.util as util +from ui.widgets import * +from wrapper.toxcore_enums_and_consts import * + + +class CreateGroupScreen(CenteredWidget): + + def __init__(self, groups_service): + super().__init__() + self._groups_service = groups_service + uic.loadUi(util.get_views_path('create_group_screen'), self) + self.center() + self.retranslateUi() + self.addGroupButton.clicked.connect(self._create_group) + self.groupNameLineEdit.textChanged.connect(self._group_name_changed) + + def retranslateUi(self): + self.setWindowTitle(util_ui.tr('Create new group chat')) + self.groupNameLabel.setText(util_ui.tr('Group name:')) + self.groupTypeLabel.setText(util_ui.tr('Group type:')) + self.groupNameLineEdit.setPlaceholderText(util_ui.tr('Group\'s persistent name')) + self.addGroupButton.setText(util_ui.tr('Create group')) + self.groupTypeComboBox.addItem(util_ui.tr('Public')) + self.groupTypeComboBox.addItem(util_ui.tr('Private')) + + def _create_group(self): + name = self.groupNameLineEdit.text() + privacy_state = self.groupTypeComboBox.currentIndex() + self._groups_service.create_new_gc(name, privacy_state) + self.close() + + def _group_name_changed(self): + name = self.groupNameLineEdit.text() + self.addGroupButton.setEnabled(bool(name.strip())) + + +class JoinGroupScreen(CenteredWidget): + + def __init__(self, groups_service): + super().__init__() + self._groups_service = groups_service + uic.loadUi(util.get_views_path('join_group_screen'), self) + self.center() + self.retranslateUi() + self.chatIdLineEdit.textChanged.connect(self._chat_id_changed) + self.joinGroupButton.clicked.connect(self._join_group) + + def retranslateUi(self): + self.setWindowTitle(util_ui.tr('Join public group chat')) + self.chatIdLabel.setText(util_ui.tr('Group ID:')) + self.passwordLabel.setText(util_ui.tr('Password:')) + self.chatIdLineEdit.setPlaceholderText(util_ui.tr('Group\'s chat ID')) + self.joinGroupButton.setText(util_ui.tr('Join group')) + self.passwordLineEdit.setPlaceholderText(util_ui.tr('Optional password')) + + def _chat_id_changed(self): + chat_id = self._get_chat_id() + self.joinGroupButton.setEnabled(len(chat_id) == TOX_GROUP_CHAT_ID_SIZE * 2) + + def _join_group(self): + chat_id = self._get_chat_id() + password = self.passwordLineEdit.text() + self._groups_service.join_gc_by_id(chat_id, password) + self.close() + + def _get_chat_id(self): + chat_id = self.chatIdLineEdit.text().strip() + if chat_id.startswith('tox:'): + chat_id = chat_id[4:] + + return chat_id diff --git a/toxygen/ui/main_screen.py b/toxygen/ui/main_screen.py index 85c2d41..fd404b2 100644 --- a/toxygen/ui/main_screen.py +++ b/toxygen/ui/main_screen.py @@ -110,6 +110,7 @@ class MainWindow(QtWidgets.QMainWindow): self.actionNetwork.triggered.connect(self.network_settings) self.actionAdd_friend.triggered.connect(self.add_contact_triggered) self.createGC.triggered.connect(self.create_gc) + self.joinGC.triggered.connect(self.join_gc) self.actionSettings.triggered.connect(self.profile_settings) self.actionPrivacy_settings.triggered.connect(self.privacy_settings) self.actionInterface_settings.triggered.connect(self.interface_settings) @@ -459,7 +460,12 @@ class MainWindow(QtWidgets.QMainWindow): self._modal_window.show() def create_gc(self): - self.profile.create_group_chat() + self._modal_window = self._widget_factory.create_group_screen_window() + self._modal_window.show() + + def join_gc(self): + self._modal_window = self._widget_factory.create_join_group_screen_window() + self._modal_window.show() def profile_settings(self, _): self._modal_window = self._widget_factory.create_profile_settings_window() diff --git a/toxygen/ui/views/create_group_screen.ui b/toxygen/ui/views/create_group_screen.ui new file mode 100644 index 0000000..08a27ff --- /dev/null +++ b/toxygen/ui/views/create_group_screen.ui @@ -0,0 +1,81 @@ + + + Form + + + + 0 + 0 + 639 + 199 + + + + Form + + + + false + + + + 180 + 150 + 271 + 41 + + + + + + + + + + 140 + 40 + 471 + 31 + + + + + + + 140 + 100 + 471 + 41 + + + + + + + 10 + 40 + 121 + 31 + + + + TextLabel + + + + + + 10 + 100 + 121 + 31 + + + + TextLabel + + + + + + diff --git a/toxygen/ui/views/join_group_screen.ui b/toxygen/ui/views/join_group_screen.ui new file mode 100644 index 0000000..66b0420 --- /dev/null +++ b/toxygen/ui/views/join_group_screen.ui @@ -0,0 +1,81 @@ + + + Form + + + + 0 + 0 + 739 + 212 + + + + Form + + + + + 30 + 40 + 67 + 17 + + + + TextLabel + + + + + + 30 + 100 + 67 + 17 + + + + TextLabel + + + + + false + + + + 258 + 150 + 241 + 51 + + + + + + + + + + 190 + 20 + 431 + 41 + + + + + + + 190 + 90 + 431 + 41 + + + + + + + diff --git a/toxygen/ui/widgets_factory.py b/toxygen/ui/widgets_factory.py index b42e342..66aca2c 100644 --- a/toxygen/ui/widgets_factory.py +++ b/toxygen/ui/widgets_factory.py @@ -1,11 +1,12 @@ from ui.main_screen_widgets import * from ui.menu import * +from ui.groups_widgets import * class WidgetsFactory: def __init__(self, settings, profile, profile_manager, contacts_manager, file_transfer_handler, smiley_loader, - plugin_loader, toxes, version): + plugin_loader, toxes, version, groups_service): self._settings = settings self._profile = profile self._profile_manager = profile_manager @@ -15,6 +16,7 @@ class WidgetsFactory: self._plugin_loader = plugin_loader self._toxes = toxes self._version = version + self._groups_service = groups_service def create_screenshot_window(self, *args): return ScreenShotWindow(self._file_transfer_handler, self._contacts_manager, *args) @@ -63,3 +65,9 @@ class WidgetsFactory: def create_sticker_window(self): return StickerWindow(self._file_transfer_handler, self._contacts_manager) + + def create_group_screen_window(self): + return CreateGroupScreen(self._groups_service) + + def create_join_group_screen_window(self): + return JoinGroupScreen(self._groups_service)