From 2d3405c5b1e0b1a76169ca4c57060083d8f79744 Mon Sep 17 00:00:00 2001 From: slowscript Date: Mon, 7 Apr 2025 22:03:45 +0200 Subject: [PATCH 1/6] Receiving text messages Display text message as a transfer in UI, show notification, allow copy text --- resources/op-item.ui | 23 ++++++++++++++++++++++ src/notifications.py | 31 ++++++++++++++++++++++++++++++ src/ops.py | 19 ++++++++++++++++++- src/server.py | 19 ++++++++++++++++++- src/warp.proto | 6 ++++++ src/warp_pb2.py | 18 ++++++++++-------- src/warp_pb2_grpc.py | 45 +++++++++++++++++++++++++++++++++++++++++++- src/warpinator.py | 22 +++++++++++++++++++--- 8 files changed, 169 insertions(+), 14 deletions(-) diff --git a/resources/op-item.ui b/resources/op-item.ui index 6936f6ea3..ffb04dcb3 100644 --- a/resources/op-item.ui +++ b/resources/op-item.ui @@ -51,6 +51,12 @@ center xsi-list-remove-symbolic + + True + False + center + edit-copy-symbolic + True False @@ -139,8 +145,10 @@ False Awaiting approval True + end 30 30 + 4 0 @@ -386,6 +394,21 @@ 8 + + + True + True + True + Copy message + center + image11 + + + False + False + 9 + + diff --git a/src/notifications.py b/src/notifications.py index cc2a7090e..f4a64c6d5 100644 --- a/src/notifications.py +++ b/src/notifications.py @@ -224,3 +224,34 @@ def _notification_response(self, action, variant, op): app = Gio.Application.get_default() app.lookup_action("notification-response").disconnect_by_func(self._notification_response) + +class TextMessageNotification(): + def __init__(self, op): + self.op = op + self.send_notification() + + @misc._idle + def send_notification(self): + if prefs.get_show_notifications(): + notification = Gio.Notification.new(_("New message from %s") % self.op.sender_name) + notification.set_body(self.op.message) + notification.set_icon(Gio.ThemedIcon(name="org.x.Warpinator-symbolic")) + notification.set_priority(Gio.NotificationPriority.URGENT) + + notification.add_button(_("Copy"), "app.notification-response::copy") + notification.set_default_action("app.notification-response::focus") + + app = Gio.Application.get_default() + app.lookup_action("notification-response").connect("activate", self._notification_response, self.op) + app.send_notification(self.op.sender, notification) + + def _notification_response(self, action, variant, op): + response = variant.unpack() + + if response == "copy": + op.copy_message() + else: + op.focus() + + app = Gio.Application.get_default() + app.lookup_action("notification-response").disconnect_by_func(self._notification_response) diff --git a/src/ops.py b/src/ops.py index df4adade4..7e9467a03 100644 --- a/src/ops.py +++ b/src/ops.py @@ -4,7 +4,7 @@ import logging from pathlib import Path -from gi.repository import GObject, GLib, Gio +from gi.repository import GObject, GLib, Gio, Gtk, Gdk import grpc @@ -283,3 +283,20 @@ def stop_transfer(self): def remove_transfer(self): self.emit("op-command", OpCommand.REMOVE_TRANSFER) +class TextMessageOp(CommonOp): + message = None + + def __init__(self, direction, sender): + super(TextMessageOp, self).__init__(direction, sender) + self.gicon = Gio.ThemedIcon.new("mail-message-new-symbolic") + self.description = _("Text message") + + def copy_message(self): + cb = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + cb.set_text(self.message, -1) + + def send_notification(self): + notifications.TextMessageNotification(self) + + def remove_transfer(self): + self.emit("op-command", OpCommand.REMOVE_TRANSFER) diff --git a/src/server.py b/src/server.py index 298603db3..a1f2cd14c 100644 --- a/src/server.py +++ b/src/server.py @@ -29,7 +29,7 @@ import util import misc import transfers -from ops import ReceiveOp +from ops import ReceiveOp, TextMessageOp from util import TransferDirection, OpStatus, RemoteStatus import zeroconf @@ -712,3 +712,20 @@ def StopTransfer(self, request, context): op.set_status(OpStatus.FAILED) return void + + def SendTextMessage(self, request, context): + logging.debug("Server RPC: SendTextMessage from '%s'" % request.ident) + try: + remote_machine:remote.RemoteMachine = self.remote_machines[request.ident] + except KeyError as e: + logging.warning("Received text message from unknown remote: %s" % e) + return + + op = TextMessageOp(TransferDirection.FROM_REMOTE_MACHINE, request.ident) + op.sender_name = remote_machine.display_name + op.message = request.message + op.status = OpStatus.FINISHED + remote_machine.add_op(op) + op.send_notification() + + return void diff --git a/src/warp.proto b/src/warp.proto index 880388777..4cac3855d 100644 --- a/src/warp.proto +++ b/src/warp.proto @@ -18,6 +18,7 @@ service Warp { rpc GetRemoteMachineAvatar(LookupName) returns (stream RemoteMachineAvatar) {} rpc ProcessTransferOpRequest(TransferOpRequest) returns (VoidType) {} rpc PauseTransferOp(OpInfo) returns (VoidType) {} + rpc SendTextMessage(TextMessage) returns (VoidType) {} // Receiver methods rpc StartTransfer(OpInfo) returns (stream FileChunk) {} @@ -114,3 +115,8 @@ message ServiceRegistration { string ipv6 = 7; } +message TextMessage { + string ident = 1; + uint64 timestamp = 2; + string message = 3; +} diff --git a/src/warp_pb2.py b/src/warp_pb2.py index 69f78764c..6a61c847d 100644 --- a/src/warp_pb2.py +++ b/src/warp_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: warp.proto -# Protobuf Python Version: 6.31.0 +# Protobuf Python Version: 5.29.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -11,8 +11,8 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 6, - 31, + 5, + 29, 0, '', 'warp.proto' @@ -24,7 +24,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nwarp.proto\"<\n\x11RemoteMachineInfo\x12\x14\n\x0c\x64isplay_name\x18\x01 \x01(\t\x12\x11\n\tuser_name\x18\x02 \x01(\t\"+\n\x13RemoteMachineAvatar\x12\x14\n\x0c\x61vatar_chunk\x18\x01 \x01(\x0c\"/\n\nLookupName\x12\n\n\x02id\x18\x01 \x01(\t\x12\x15\n\rreadable_name\x18\x02 \x01(\t\"\x1e\n\nHaveDuplex\x12\x10\n\x08response\x18\x02 \x01(\x08\"\x19\n\x08VoidType\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\"Z\n\x06OpInfo\x12\r\n\x05ident\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x15\n\rreadable_name\x18\x03 \x01(\t\x12\x17\n\x0fuse_compression\x18\x04 \x01(\x08\"0\n\x08StopInfo\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\r\n\x05\x65rror\x18\x02 \x01(\x08\"\xd0\x01\n\x11TransferOpRequest\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\x13\n\x0bsender_name\x18\x02 \x01(\t\x12\x15\n\rreceiver_name\x18\x03 \x01(\t\x12\x10\n\x08receiver\x18\x04 \x01(\t\x12\x0c\n\x04size\x18\x05 \x01(\x04\x12\r\n\x05\x63ount\x18\x06 \x01(\x04\x12\x16\n\x0ename_if_single\x18\x07 \x01(\t\x12\x16\n\x0emime_if_single\x18\x08 \x01(\t\x12\x19\n\x11top_dir_basenames\x18\t \x03(\t\"\x88\x01\n\tFileChunk\x12\x15\n\rrelative_path\x18\x01 \x01(\t\x12\x11\n\tfile_type\x18\x02 \x01(\x05\x12\x16\n\x0esymlink_target\x18\x03 \x01(\t\x12\r\n\x05\x63hunk\x18\x04 \x01(\x0c\x12\x11\n\tfile_mode\x18\x05 \x01(\r\x12\x17\n\x04time\x18\x06 \x01(\x0b\x32\t.FileTime\"-\n\x08\x46ileTime\x12\r\n\x05mtime\x18\x01 \x01(\x04\x12\x12\n\nmtime_usec\x18\x02 \x01(\r\"8\n\nRegRequest\x12\n\n\x02ip\x18\x01 \x01(\t\x12\x10\n\x08hostname\x18\x02 \x01(\t\x12\x0c\n\x04ipv6\x18\x03 \x01(\t\"\"\n\x0bRegResponse\x12\x13\n\x0blocked_cert\x18\x01 \x01(\t\"\x8b\x01\n\x13ServiceRegistration\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\x12\x0c\n\x04port\x18\x03 \x01(\r\x12\x10\n\x08hostname\x18\x04 \x01(\t\x12\x13\n\x0b\x61pi_version\x18\x05 \x01(\r\x12\x11\n\tauth_port\x18\x06 \x01(\r\x12\x0c\n\x04ipv6\x18\x07 \x01(\t2\xf2\x03\n\x04Warp\x12\x33\n\x15\x43heckDuplexConnection\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12.\n\x10WaitingForDuplex\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12\x39\n\x14GetRemoteMachineInfo\x12\x0b.LookupName\x1a\x12.RemoteMachineInfo\"\x00\x12?\n\x16GetRemoteMachineAvatar\x12\x0b.LookupName\x1a\x14.RemoteMachineAvatar\"\x00\x30\x01\x12;\n\x18ProcessTransferOpRequest\x12\x12.TransferOpRequest\x1a\t.VoidType\"\x00\x12\'\n\x0fPauseTransferOp\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12(\n\rStartTransfer\x12\x07.OpInfo\x1a\n.FileChunk\"\x00\x30\x01\x12/\n\x17\x43\x61ncelTransferOpRequest\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12&\n\x0cStopTransfer\x12\t.StopInfo\x1a\t.VoidType\"\x00\x12 \n\x04Ping\x12\x0b.LookupName\x1a\t.VoidType\"\x00\x32\x86\x01\n\x10WarpRegistration\x12\x31\n\x12RequestCertificate\x12\x0b.RegRequest\x1a\x0c.RegResponse\"\x00\x12?\n\x0fRegisterService\x12\x14.ServiceRegistration\x1a\x14.ServiceRegistration\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nwarp.proto\"<\n\x11RemoteMachineInfo\x12\x14\n\x0c\x64isplay_name\x18\x01 \x01(\t\x12\x11\n\tuser_name\x18\x02 \x01(\t\"+\n\x13RemoteMachineAvatar\x12\x14\n\x0c\x61vatar_chunk\x18\x01 \x01(\x0c\"/\n\nLookupName\x12\n\n\x02id\x18\x01 \x01(\t\x12\x15\n\rreadable_name\x18\x02 \x01(\t\"\x1e\n\nHaveDuplex\x12\x10\n\x08response\x18\x02 \x01(\x08\"\x19\n\x08VoidType\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\"Z\n\x06OpInfo\x12\r\n\x05ident\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x15\n\rreadable_name\x18\x03 \x01(\t\x12\x17\n\x0fuse_compression\x18\x04 \x01(\x08\"0\n\x08StopInfo\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\r\n\x05\x65rror\x18\x02 \x01(\x08\"\xd0\x01\n\x11TransferOpRequest\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\x13\n\x0bsender_name\x18\x02 \x01(\t\x12\x15\n\rreceiver_name\x18\x03 \x01(\t\x12\x10\n\x08receiver\x18\x04 \x01(\t\x12\x0c\n\x04size\x18\x05 \x01(\x04\x12\r\n\x05\x63ount\x18\x06 \x01(\x04\x12\x16\n\x0ename_if_single\x18\x07 \x01(\t\x12\x16\n\x0emime_if_single\x18\x08 \x01(\t\x12\x19\n\x11top_dir_basenames\x18\t \x03(\t\"\x88\x01\n\tFileChunk\x12\x15\n\rrelative_path\x18\x01 \x01(\t\x12\x11\n\tfile_type\x18\x02 \x01(\x05\x12\x16\n\x0esymlink_target\x18\x03 \x01(\t\x12\r\n\x05\x63hunk\x18\x04 \x01(\x0c\x12\x11\n\tfile_mode\x18\x05 \x01(\r\x12\x17\n\x04time\x18\x06 \x01(\x0b\x32\t.FileTime\"-\n\x08\x46ileTime\x12\r\n\x05mtime\x18\x01 \x01(\x04\x12\x12\n\nmtime_usec\x18\x02 \x01(\r\"8\n\nRegRequest\x12\n\n\x02ip\x18\x01 \x01(\t\x12\x10\n\x08hostname\x18\x02 \x01(\t\x12\x0c\n\x04ipv6\x18\x03 \x01(\t\"\"\n\x0bRegResponse\x12\x13\n\x0blocked_cert\x18\x01 \x01(\t\"\x8b\x01\n\x13ServiceRegistration\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\x12\x0c\n\x04port\x18\x03 \x01(\r\x12\x10\n\x08hostname\x18\x04 \x01(\t\x12\x13\n\x0b\x61pi_version\x18\x05 \x01(\r\x12\x11\n\tauth_port\x18\x06 \x01(\r\x12\x0c\n\x04ipv6\x18\x07 \x01(\t\"@\n\x0bTextMessage\x12\r\n\x05ident\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x0f\n\x07message\x18\x03 \x01(\t2\xa0\x04\n\x04Warp\x12\x33\n\x15\x43heckDuplexConnection\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12.\n\x10WaitingForDuplex\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12\x39\n\x14GetRemoteMachineInfo\x12\x0b.LookupName\x1a\x12.RemoteMachineInfo\"\x00\x12?\n\x16GetRemoteMachineAvatar\x12\x0b.LookupName\x1a\x14.RemoteMachineAvatar\"\x00\x30\x01\x12;\n\x18ProcessTransferOpRequest\x12\x12.TransferOpRequest\x1a\t.VoidType\"\x00\x12\'\n\x0fPauseTransferOp\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12,\n\x0fSendTextMessage\x12\x0c.TextMessage\x1a\t.VoidType\"\x00\x12(\n\rStartTransfer\x12\x07.OpInfo\x1a\n.FileChunk\"\x00\x30\x01\x12/\n\x17\x43\x61ncelTransferOpRequest\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12&\n\x0cStopTransfer\x12\t.StopInfo\x1a\t.VoidType\"\x00\x12 \n\x04Ping\x12\x0b.LookupName\x1a\t.VoidType\"\x00\x32\x86\x01\n\x10WarpRegistration\x12\x31\n\x12RequestCertificate\x12\x0b.RegRequest\x1a\x0c.RegResponse\"\x00\x12?\n\x0fRegisterService\x12\x14.ServiceRegistration\x1a\x14.ServiceRegistration\"\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -57,8 +57,10 @@ _globals['_REGRESPONSE']._serialized_end=860 _globals['_SERVICEREGISTRATION']._serialized_start=863 _globals['_SERVICEREGISTRATION']._serialized_end=1002 - _globals['_WARP']._serialized_start=1005 - _globals['_WARP']._serialized_end=1503 - _globals['_WARPREGISTRATION']._serialized_start=1506 - _globals['_WARPREGISTRATION']._serialized_end=1640 + _globals['_TEXTMESSAGE']._serialized_start=1004 + _globals['_TEXTMESSAGE']._serialized_end=1068 + _globals['_WARP']._serialized_start=1071 + _globals['_WARP']._serialized_end=1615 + _globals['_WARPREGISTRATION']._serialized_start=1618 + _globals['_WARPREGISTRATION']._serialized_end=1752 # @@protoc_insertion_point(module_scope) diff --git a/src/warp_pb2_grpc.py b/src/warp_pb2_grpc.py index 218fd5ab0..0ebb8488d 100644 --- a/src/warp_pb2_grpc.py +++ b/src/warp_pb2_grpc.py @@ -5,7 +5,7 @@ import warp_pb2 as warp__pb2 -GRPC_GENERATED_VERSION = '1.73.1' +GRPC_GENERATED_VERSION = '1.71.0' GRPC_VERSION = grpc.__version__ _version_not_supported = False @@ -71,6 +71,11 @@ def __init__(self, channel): request_serializer=warp__pb2.OpInfo.SerializeToString, response_deserializer=warp__pb2.VoidType.FromString, _registered_method=True) + self.SendTextMessage = channel.unary_unary( + '/Warp/SendTextMessage', + request_serializer=warp__pb2.TextMessage.SerializeToString, + response_deserializer=warp__pb2.VoidType.FromString, + _registered_method=True) self.StartTransfer = channel.unary_stream( '/Warp/StartTransfer', request_serializer=warp__pb2.OpInfo.SerializeToString, @@ -142,6 +147,12 @@ def PauseTransferOp(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def SendTextMessage(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def StartTransfer(self, request, context): """Receiver methods """ @@ -201,6 +212,11 @@ def add_WarpServicer_to_server(servicer, server): request_deserializer=warp__pb2.OpInfo.FromString, response_serializer=warp__pb2.VoidType.SerializeToString, ), + 'SendTextMessage': grpc.unary_unary_rpc_method_handler( + servicer.SendTextMessage, + request_deserializer=warp__pb2.TextMessage.FromString, + response_serializer=warp__pb2.VoidType.SerializeToString, + ), 'StartTransfer': grpc.unary_stream_rpc_method_handler( servicer.StartTransfer, request_deserializer=warp__pb2.OpInfo.FromString, @@ -401,6 +417,33 @@ def PauseTransferOp(request, metadata, _registered_method=True) + @staticmethod + def SendTextMessage(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/Warp/SendTextMessage', + warp__pb2.TextMessage.SerializeToString, + warp__pb2.VoidType.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + @staticmethod def StartTransfer(request, target, diff --git a/src/warpinator.py b/src/warpinator.py index c69de2931..32fcdb084 100644 --- a/src/warpinator.py +++ b/src/warpinator.py @@ -27,7 +27,7 @@ import auth import misc import networkmonitor -from ops import SendOp, ReceiveOp +from ops import SendOp, ReceiveOp, TextMessageOp from util import TransferDirection, OpStatus, RemoteStatus # XApp 2.0 required for favorites. @@ -67,7 +67,8 @@ "transfer_resume", \ "transfer_stop", \ "transfer_remove", \ - "transfer_open_folder") + "transfer_open_folder", \ + "transfer_copy_message") INIT_BUTTONS = () PERM_TO_SEND_BUTTONS = ("transfer_cancel_request",) @@ -86,6 +87,7 @@ TRANSFER_COMPLETED_SENDER_BUTTONS = TRANSFER_CANCELLED_BUTTONS TRANSFER_FILE_NOT_FOUND_BUTTONS = TRANSFER_CANCELLED_BUTTONS TRANSFER_COMPLETED_RECEIVER_BUTTONS = ("transfer_remove", "transfer_open_folder") +TRANSFER_TEXT_MESSAGE_BUTTONS = ("transfer_remove", "transfer_copy_message") class OpItem(object): def __init__(self, op): @@ -111,6 +113,7 @@ def __init__(self, op): self.stop_button = self.builder.get_object("transfer_stop") self.remove_button = self.builder.get_object("transfer_remove") self.folder_button = self.builder.get_object("transfer_open_folder") + self.copy_button = self.builder.get_object("transfer_copy_message") self.accept_button.connect("clicked", self.accept_button_clicked) self.decline_button.connect("clicked", self.decline_button_clicked) @@ -120,6 +123,7 @@ def __init__(self, op): self.stop_button.connect("clicked", self.stop_button_clicked) self.remove_button.connect("clicked", self.remove_button_clicked) self.folder_button.connect("clicked", self.folder_button_clicked) + self.copy_button.connect("clicked", self.copy_button_clicked) self.op.connect("progress-changed", self.update_progress) @@ -181,7 +185,14 @@ def refresh_status_widgets(self): else: self.op_transfer_problem_label.set_text(_("Some files not found")) elif self.op.status == OpStatus.FINISHED: - self.op_transfer_status_message.set_text(_("Completed")) + if isinstance(self.op, TextMessageOp): + msg = "\n".join(self.op.message.split("\n")[:4]) # Max 4 lines + if len(msg) > 120: # Max 120 chars (4*30 per line) -- FIXME: This might still exceed 4 lines + msg = msg[:117] + "..." + self.op_transfer_status_message.set_text(msg) + self.op_transfer_status_message.set_selectable(True) + else: + self.op_transfer_status_message.set_text(_("Completed")) elif self.op.status == OpStatus.FINISHED_WARNING: self.op_transfer_status_message.set_text(_("Completed, but with errors")) @@ -237,6 +248,8 @@ def refresh_buttons_and_icons(self): self.op_status_stack.set_visible_child_name("message") if isinstance(self.op, SendOp): self.set_visible_buttons(TRANSFER_COMPLETED_SENDER_BUTTONS) + elif isinstance(self.op, TextMessageOp): + self.set_visible_buttons(TRANSFER_TEXT_MESSAGE_BUTTONS) else: self.set_visible_buttons(TRANSFER_COMPLETED_RECEIVER_BUTTONS) elif self.op.status in (OpStatus.CANCELLED_PERMISSION_BY_SENDER, @@ -275,6 +288,9 @@ def folder_button_clicked(self, button): util.open_save_folder(self.op.top_dir_basenames[0]) else: util.open_save_folder() + + def copy_button_clicked(self, button): + self.op.copy_message() def destroy(self): self.builder = None From eaf7e81ffa5893e524e9205860adcd8fe23e0796 Mon Sep 17 00:00:00 2001 From: slowscript Date: Mon, 7 Apr 2025 22:05:44 +0200 Subject: [PATCH 2/6] Sending text messages Send message button and dialog --- resources/main-window.ui | 20 ++++++++++++++ src/ops.py | 3 +++ src/remote.py | 26 ++++++++++++++++--- src/warpinator.py | 56 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 3 deletions(-) diff --git a/resources/main-window.ui b/resources/main-window.ui index f9cf913f6..38f888128 100644 --- a/resources/main-window.ui +++ b/resources/main-window.ui @@ -592,6 +592,26 @@ 1 + + + True + True + False + + + True + False + Send message + + + + + True + True + end + 2 + + diff --git a/src/ops.py b/src/ops.py index 7e9467a03..be77e4689 100644 --- a/src/ops.py +++ b/src/ops.py @@ -300,3 +300,6 @@ def send_notification(self): def remove_transfer(self): self.emit("op-command", OpCommand.REMOVE_TRANSFER) + + def retry_transfer(self): + self.emit("op-command", OpCommand.RETRY_TRANSFER) diff --git a/src/remote.py b/src/remote.py index 0ac1f0f7b..b6664fe49 100644 --- a/src/remote.py +++ b/src/remote.py @@ -18,7 +18,7 @@ import misc import transfers import auth -from ops import SendOp, ReceiveOp +from ops import SendOp, ReceiveOp, TextMessageOp from util import TransferDirection, OpStatus, OpCommand, RemoteStatus, ReceiveError _ = gettext.gettext @@ -590,6 +590,21 @@ def _send_files(uri_list): util.add_to_recents_if_single_selection(uri_list) self.rpc_call(_send_files, uri_list) + def send_text_message(self, message): + op = TextMessageOp(TransferDirection.TO_REMOTE_MACHINE, self.local_ident) + op.message = message + op.status = OpStatus.FINISHED + self.add_op(op) + self.rpc_call(self.do_send_text_message, op) + + def do_send_text_message(self, op): + try: + self.stub.SendTextMessage(warp_pb2.TextMessage(ident=self.local_ident, timestamp=op.start_time, message=op.message)) + except Exception as e: + logging.error("Sending message failed: %s" % e) + op.status = OpStatus.FAILED + op.emit_status_changed() + @misc._idle def add_op(self, op): if op not in self.transfer_ops: @@ -662,8 +677,13 @@ def op_command_issued(self, op, command): elif command == OpCommand.STOP_TRANSFER_BY_SENDER: self.rpc_call(self.stop_transfer_op, op, by_sender=True) elif command == OpCommand.RETRY_TRANSFER: - op.set_status(OpStatus.WAITING_PERMISSION) - self.rpc_call(self.send_transfer_op_request, op) + if isinstance(op, TextMessageOp): + op.status = OpStatus.FINISHED + op.emit_status_changed() + self.rpc_call(self.do_send_text_message, op) + else: + op.set_status(OpStatus.WAITING_PERMISSION) + self.rpc_call(self.send_transfer_op_request, op) elif command == OpCommand.REMOVE_TRANSFER: self.remove_op(op) # receive diff --git a/src/warpinator.py b/src/warpinator.py index 32fcdb084..fdbbff570 100644 --- a/src/warpinator.py +++ b/src/warpinator.py @@ -522,6 +522,8 @@ def __init__(self): self.user_ip_label = self.builder.get_object("user_ip") self.user_op_list = self.builder.get_object("user_op_list") self.user_send_button = self.builder.get_object("user_send_button") + self.user_send_msg_button = self.builder.get_object("user_send_msg_button") + self.user_send_msg_button.connect("clicked", self.send_msg_button_clicked) self.user_online_box = self.builder.get_object("user_online_box") self.user_online_image = self.builder.get_object("user_online_image") self.user_online_label = self.builder.get_object("user_online_label") @@ -752,6 +754,9 @@ def recent_item_selected(self, recent_chooser, data=None): def favorite_selected(self, favorites, uri): self.current_selected_remote_machine.send_files([uri]) + def send_msg_button_clicked(self, button): + SendMessageDialog(self).show() + def open_file_picker(self, button, data=None): dialog = util.create_file_and_folder_picker(self.window) @@ -865,6 +870,9 @@ def restart_service_clicked(self, menuitem): def manual_connect_to_host(self, host): logging.debug("Connecting to " + host) + def send_text_message(self, message): + self.current_selected_remote_machine.send_text_message(message) + def report_bad_save_folder(self): path = prefs.get_save_path() self.bad_save_folder_label.set_text(path) @@ -1069,6 +1077,7 @@ def current_selected_remote_status_changed(self, remote_machine): (entry,), Gdk.DragAction.COPY) self.user_send_button.set_sensitive(True) + self.user_send_msg_button.set_sensitive(True) self.user_online_label.set_text(_("Online")) self.user_online_image.set_from_icon_name(ICON_ONLINE, Gtk.IconSize.LARGE_TOOLBAR) self.user_online_spinner.hide() @@ -1076,6 +1085,7 @@ def current_selected_remote_status_changed(self, remote_machine): elif remote_machine.status == RemoteStatus.OFFLINE: self.user_op_list.drag_dest_unset() self.user_send_button.set_sensitive(False) + self.user_send_msg_button.set_sensitive(False) self.user_online_label.set_text(_("Offline")) self.user_online_image.set_from_icon_name(ICON_OFFLINE, Gtk.IconSize.LARGE_TOOLBAR) self.user_online_spinner.hide() @@ -1083,6 +1093,7 @@ def current_selected_remote_status_changed(self, remote_machine): elif remote_machine.status == RemoteStatus.UNREACHABLE: self.user_op_list.drag_dest_unset() self.user_send_button.set_sensitive(False) + self.user_send_msg_button.set_sensitive(False) self.user_online_label.set_text(_("Unable to connect")) self.user_online_image.set_from_icon_name(ICON_UNREACHABLE, Gtk.IconSize.LARGE_TOOLBAR) self.user_online_spinner.hide() @@ -1090,6 +1101,7 @@ def current_selected_remote_status_changed(self, remote_machine): elif remote_machine.status == RemoteStatus.AWAITING_DUPLEX: self.user_op_list.drag_dest_unset() self.user_send_button.set_sensitive(False) + self.user_send_msg_button.set_sensitive(False) self.user_online_label.set_text(_("Waiting for two-way connection")) self.user_online_image.set_from_icon_name(ICON_UNREACHABLE, Gtk.IconSize.LARGE_TOOLBAR) self.user_online_spinner.hide() @@ -1097,6 +1109,7 @@ def current_selected_remote_status_changed(self, remote_machine): else: self.user_op_list.drag_dest_unset() self.user_send_button.set_sensitive(False) + self.user_send_msg_button.set_sensitive(False) self.user_online_label.set_text(_("Connecting")) self.user_online_image.hide() self.user_online_spinner.show() @@ -1310,6 +1323,49 @@ def validate_address(self, entry): except: self.connect_button.set_sensitive(False) +class SendMessageDialog(Gtk.Window): + def __init__(self, parent:WarpWindow): + super().__init__(title=_("Send message"), transient_for=parent.window, modal=True, resizable=False) + self.parent = parent + + self.set_default_size(300, 100) + self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + self.set_type_hint(Gdk.WindowTypeHint.DIALOG) + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + self.add(vbox) + + scrollView = Gtk.ScrolledWindow() + scrollView.set_size_request(300, 50) + scrollView.set_shadow_type(Gtk.ShadowType.OUT) + vbox.add(scrollView) + + self.textView = Gtk.TextView() + self.textView.set_editable(True) + self.textView.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + scrollView.add(self.textView) + + btnClose = Gtk.Button(_("Cancel")) + btnClose.connect("clicked", lambda _ : self.close()) + btnSend = Gtk.Button(_("Send")) + btnSend.connect("clicked", self.send_clicked) + btnBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + btnBox.pack_end(btnSend, False, False, 0) + btnBox.pack_end(btnClose, False, False, 0) + vbox.add(btnBox) + + vbox.set_margin_bottom(10) + vbox.set_margin_top(10) + vbox.set_margin_left(10) + vbox.set_margin_right(10) + self.show_all() + + def send_clicked(self, btn): + buf = self.textView.get_buffer() + buf_s, buf_e = buf.get_bounds() + self.parent.send_text_message(buf.get_text(buf_s, buf_e, False)) + self.close() + class WarpApplication(Gtk.Application): def __init__(self, testing=False): super(WarpApplication, self).__init__(application_id="org.x.Warpinator", register_session=True) From d1ea9e389f0b4f0ea34212268d336e58cee0f81d Mon Sep 17 00:00:00 2001 From: slowscript Date: Sat, 15 Nov 2025 18:08:00 +0100 Subject: [PATCH 3/6] Enter message inside main window instead of a dialog --- resources/main-window.ui | 69 ++++++++++++++++++++++++++++------------ resources/op-item.ui | 4 +-- src/ops.py | 2 +- src/warpinator.py | 63 ++++++++++-------------------------- 4 files changed, 66 insertions(+), 72 deletions(-) diff --git a/resources/main-window.ui b/resources/main-window.ui index 38f888128..58e34f4fa 100644 --- a/resources/main-window.ui +++ b/resources/main-window.ui @@ -18,6 +18,11 @@ False xsi-window-close-symbolic + + True + False + xsi-mail-send + True False @@ -592,26 +597,6 @@ 1 - - - True - True - False - - - True - False - Send message - - - - - True - True - end - 2 - - @@ -720,9 +705,51 @@ False True - 2 + 1 + + + True + False + 6 + + + 40 + True + True + never + external + out + + + True + True + word-char + + + + + True + True + 0 + + + + + True + True + False + image4 + + + False + True + 1 + + + + False diff --git a/resources/op-item.ui b/resources/op-item.ui index ffb04dcb3..039823ac4 100644 --- a/resources/op-item.ui +++ b/resources/op-item.ui @@ -55,7 +55,7 @@ True False center - edit-copy-symbolic + xsi-edit-copy-symbolic True @@ -145,10 +145,8 @@ False Awaiting approval True - end 30 30 - 4 0 diff --git a/src/ops.py b/src/ops.py index be77e4689..1726bda58 100644 --- a/src/ops.py +++ b/src/ops.py @@ -288,7 +288,7 @@ class TextMessageOp(CommonOp): def __init__(self, direction, sender): super(TextMessageOp, self).__init__(direction, sender) - self.gicon = Gio.ThemedIcon.new("mail-message-new-symbolic") + self.gicon = Gio.ThemedIcon.new("xsi-mail-message-new-symbolic") self.description = _("Text message") def copy_message(self): diff --git a/src/warpinator.py b/src/warpinator.py index fdbbff570..c9eae5894 100644 --- a/src/warpinator.py +++ b/src/warpinator.py @@ -524,6 +524,8 @@ def __init__(self): self.user_send_button = self.builder.get_object("user_send_button") self.user_send_msg_button = self.builder.get_object("user_send_msg_button") self.user_send_msg_button.connect("clicked", self.send_msg_button_clicked) + self.user_msg_entry = self.builder.get_object("user_msg_entry") + self.user_msg_entry.connect("key-press-event", self.msg_entry_key_press) self.user_online_box = self.builder.get_object("user_online_box") self.user_online_image = self.builder.get_object("user_online_image") self.user_online_label = self.builder.get_object("user_online_label") @@ -651,7 +653,7 @@ def window_delete_event(self, widget, event, data=None): def window_key_press(self, widget, event, data=None): if not self.search_entry.has_focus() and self.view_stack.get_visible_child_name() == "overview": self.search_entry.grab_focus() - elif event.keyval == Gdk.KEY_BackSpace and self.view_stack.get_visible_child_name() == "user": + elif event.keyval == Gdk.KEY_BackSpace and self.view_stack.get_visible_child_name() == "user" and not self.user_msg_entry.has_focus(): self.back_to_overview() return Gdk.EVENT_STOP @@ -755,7 +757,14 @@ def favorite_selected(self, favorites, uri): self.current_selected_remote_machine.send_files([uri]) def send_msg_button_clicked(self, button): - SendMessageDialog(self).show() + self.send_text_message() + + def msg_entry_key_press(self, entry, event, data=None): + if event.keyval == Gdk.KEY_Return and event.state & Gdk.ModifierType.CONTROL_MASK: + self.send_text_message() + return Gdk.EVENT_STOP + + return Gdk.EVENT_PROPAGATE def open_file_picker(self, button, data=None): dialog = util.create_file_and_folder_picker(self.window) @@ -870,8 +879,11 @@ def restart_service_clicked(self, menuitem): def manual_connect_to_host(self, host): logging.debug("Connecting to " + host) - def send_text_message(self, message): - self.current_selected_remote_machine.send_text_message(message) + def send_text_message(self): + buf = self.user_msg_entry.get_buffer() + buf_s, buf_e = buf.get_bounds() + self.current_selected_remote_machine.send_text_message(buf.get_text(buf_s, buf_e, False)) + buf.delete(buf_s, buf_e) def report_bad_save_folder(self): path = prefs.get_save_path() @@ -1323,49 +1335,6 @@ def validate_address(self, entry): except: self.connect_button.set_sensitive(False) -class SendMessageDialog(Gtk.Window): - def __init__(self, parent:WarpWindow): - super().__init__(title=_("Send message"), transient_for=parent.window, modal=True, resizable=False) - self.parent = parent - - self.set_default_size(300, 100) - self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) - self.set_type_hint(Gdk.WindowTypeHint.DIALOG) - - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) - self.add(vbox) - - scrollView = Gtk.ScrolledWindow() - scrollView.set_size_request(300, 50) - scrollView.set_shadow_type(Gtk.ShadowType.OUT) - vbox.add(scrollView) - - self.textView = Gtk.TextView() - self.textView.set_editable(True) - self.textView.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - scrollView.add(self.textView) - - btnClose = Gtk.Button(_("Cancel")) - btnClose.connect("clicked", lambda _ : self.close()) - btnSend = Gtk.Button(_("Send")) - btnSend.connect("clicked", self.send_clicked) - btnBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) - btnBox.pack_end(btnSend, False, False, 0) - btnBox.pack_end(btnClose, False, False, 0) - vbox.add(btnBox) - - vbox.set_margin_bottom(10) - vbox.set_margin_top(10) - vbox.set_margin_left(10) - vbox.set_margin_right(10) - self.show_all() - - def send_clicked(self, btn): - buf = self.textView.get_buffer() - buf_s, buf_e = buf.get_bounds() - self.parent.send_text_message(buf.get_text(buf_s, buf_e, False)) - self.close() - class WarpApplication(Gtk.Application): def __init__(self, testing=False): super(WarpApplication, self).__init__(application_id="org.x.Warpinator", register_session=True) From 0b2b28c3c64cb3422728e807361630ada401d7a4 Mon Sep 17 00:00:00 2001 From: slowscript Date: Sat, 15 Nov 2025 18:19:45 +0100 Subject: [PATCH 4/6] Only allow sending messages when remote supports them --- src/remote.py | 5 +++- src/server.py | 7 ++++-- src/util.py | 5 +++- src/warp.proto | 1 + src/warp_pb2.py | 64 +++++++++++++++++++++++------------------------ src/warpinator.py | 3 +++ 6 files changed, 49 insertions(+), 36 deletions(-) diff --git a/src/remote.py b/src/remote.py index b6664fe49..f7123802b 100644 --- a/src/remote.py +++ b/src/remote.py @@ -19,7 +19,7 @@ import transfers import auth from ops import SendOp, ReceiveOp, TextMessageOp -from util import TransferDirection, OpStatus, OpCommand, RemoteStatus, ReceiveError +from util import TransferDirection, OpStatus, OpCommand, RemoteStatus, ReceiveError, RemoteFeatures _ = gettext.gettext @@ -56,6 +56,7 @@ def __init__(self, ident, hostname, display_hostname, ip_info, port, local_ident self.display_name = "" self.favorite = prefs.get_is_favorite(self.ident) self.recent_time = 0 # Keep monotonic time when visited on the user page + self.supports_messages = False self.avatar_surface = None self.transfer_ops = [] @@ -366,6 +367,8 @@ def get_info_finished(future): info = future.result() self.display_name = info.display_name self.user_name = info.user_name + feature_flags = RemoteFeatures(info.feature_flags) + self.supports_messages = RemoteFeatures.TEXT_MESSAGES in feature_flags self.favorite = prefs.get_is_favorite(self.ident) valid = GLib.utf8_make_valid(self.display_name, -1) diff --git a/src/server.py b/src/server.py index a1f2cd14c..053f42098 100644 --- a/src/server.py +++ b/src/server.py @@ -30,7 +30,7 @@ import misc import transfers from ops import ReceiveOp, TextMessageOp -from util import TransferDirection, OpStatus, RemoteStatus +from util import TransferDirection, OpStatus, RemoteStatus, RemoteFeatures import zeroconf from zeroconf import ServiceInfo, Zeroconf, ServiceBrowser, IPVersion @@ -41,6 +41,8 @@ SERVICE_TYPE = "_warpinator._tcp.local." +SERVER_FEATURES = RemoteFeatures.TEXT_MESSAGES + # server (this is on a separate thread from the ui, grpc isn't compatible with # gmainloop) class Server(threading.Thread, warp_pb2_grpc.WarpServicer, GObject.Object): @@ -559,7 +561,8 @@ def GetRemoteMachineInfo(self, request, context): logging.debug("Server RPC: GetRemoteMachineInfo from '%s'" % request.readable_name) return warp_pb2.RemoteMachineInfo(display_name=GLib.get_real_name(), - user_name=GLib.get_user_name()) + user_name=GLib.get_user_name(), + feature_flags=SERVER_FEATURES) def GetRemoteMachineAvatar(self, request, context): logging.debug("Server RPC: GetRemoteMachineAvatar from '%s'" % request.readable_name) diff --git a/src/util.py b/src/util.py index de1c6fe2a..7fb73909b 100644 --- a/src/util.py +++ b/src/util.py @@ -141,7 +141,7 @@ def shutdown(self, wait=True): self._factory_thread.join() logging.debug("NewThreadExecutor: Shutdown complete") -from enum import IntEnum +from enum import IntEnum, IntFlag TransferDirection = IntEnum('TransferDirection', 'TO_REMOTE_MACHINE \ FROM_REMOTE_MACHINE') @@ -192,6 +192,9 @@ def shutdown(self, wait=True): CERT_UP_TO_DATE \ FAILURE') +class RemoteFeatures(IntFlag): + TEXT_MESSAGES = 1 << 0 + class ReceiveError(Exception): def __init__(self, message, fatal=True): self.fatal = fatal diff --git a/src/warp.proto b/src/warp.proto index 4cac3855d..9ce943339 100644 --- a/src/warp.proto +++ b/src/warp.proto @@ -33,6 +33,7 @@ service Warp { message RemoteMachineInfo { string display_name = 1; string user_name = 2; + uint32 feature_flags = 3; } message RemoteMachineAvatar { diff --git a/src/warp_pb2.py b/src/warp_pb2.py index 6a61c847d..6a595ef1b 100644 --- a/src/warp_pb2.py +++ b/src/warp_pb2.py @@ -24,7 +24,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nwarp.proto\"<\n\x11RemoteMachineInfo\x12\x14\n\x0c\x64isplay_name\x18\x01 \x01(\t\x12\x11\n\tuser_name\x18\x02 \x01(\t\"+\n\x13RemoteMachineAvatar\x12\x14\n\x0c\x61vatar_chunk\x18\x01 \x01(\x0c\"/\n\nLookupName\x12\n\n\x02id\x18\x01 \x01(\t\x12\x15\n\rreadable_name\x18\x02 \x01(\t\"\x1e\n\nHaveDuplex\x12\x10\n\x08response\x18\x02 \x01(\x08\"\x19\n\x08VoidType\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\"Z\n\x06OpInfo\x12\r\n\x05ident\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x15\n\rreadable_name\x18\x03 \x01(\t\x12\x17\n\x0fuse_compression\x18\x04 \x01(\x08\"0\n\x08StopInfo\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\r\n\x05\x65rror\x18\x02 \x01(\x08\"\xd0\x01\n\x11TransferOpRequest\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\x13\n\x0bsender_name\x18\x02 \x01(\t\x12\x15\n\rreceiver_name\x18\x03 \x01(\t\x12\x10\n\x08receiver\x18\x04 \x01(\t\x12\x0c\n\x04size\x18\x05 \x01(\x04\x12\r\n\x05\x63ount\x18\x06 \x01(\x04\x12\x16\n\x0ename_if_single\x18\x07 \x01(\t\x12\x16\n\x0emime_if_single\x18\x08 \x01(\t\x12\x19\n\x11top_dir_basenames\x18\t \x03(\t\"\x88\x01\n\tFileChunk\x12\x15\n\rrelative_path\x18\x01 \x01(\t\x12\x11\n\tfile_type\x18\x02 \x01(\x05\x12\x16\n\x0esymlink_target\x18\x03 \x01(\t\x12\r\n\x05\x63hunk\x18\x04 \x01(\x0c\x12\x11\n\tfile_mode\x18\x05 \x01(\r\x12\x17\n\x04time\x18\x06 \x01(\x0b\x32\t.FileTime\"-\n\x08\x46ileTime\x12\r\n\x05mtime\x18\x01 \x01(\x04\x12\x12\n\nmtime_usec\x18\x02 \x01(\r\"8\n\nRegRequest\x12\n\n\x02ip\x18\x01 \x01(\t\x12\x10\n\x08hostname\x18\x02 \x01(\t\x12\x0c\n\x04ipv6\x18\x03 \x01(\t\"\"\n\x0bRegResponse\x12\x13\n\x0blocked_cert\x18\x01 \x01(\t\"\x8b\x01\n\x13ServiceRegistration\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\x12\x0c\n\x04port\x18\x03 \x01(\r\x12\x10\n\x08hostname\x18\x04 \x01(\t\x12\x13\n\x0b\x61pi_version\x18\x05 \x01(\r\x12\x11\n\tauth_port\x18\x06 \x01(\r\x12\x0c\n\x04ipv6\x18\x07 \x01(\t\"@\n\x0bTextMessage\x12\r\n\x05ident\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x0f\n\x07message\x18\x03 \x01(\t2\xa0\x04\n\x04Warp\x12\x33\n\x15\x43heckDuplexConnection\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12.\n\x10WaitingForDuplex\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12\x39\n\x14GetRemoteMachineInfo\x12\x0b.LookupName\x1a\x12.RemoteMachineInfo\"\x00\x12?\n\x16GetRemoteMachineAvatar\x12\x0b.LookupName\x1a\x14.RemoteMachineAvatar\"\x00\x30\x01\x12;\n\x18ProcessTransferOpRequest\x12\x12.TransferOpRequest\x1a\t.VoidType\"\x00\x12\'\n\x0fPauseTransferOp\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12,\n\x0fSendTextMessage\x12\x0c.TextMessage\x1a\t.VoidType\"\x00\x12(\n\rStartTransfer\x12\x07.OpInfo\x1a\n.FileChunk\"\x00\x30\x01\x12/\n\x17\x43\x61ncelTransferOpRequest\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12&\n\x0cStopTransfer\x12\t.StopInfo\x1a\t.VoidType\"\x00\x12 \n\x04Ping\x12\x0b.LookupName\x1a\t.VoidType\"\x00\x32\x86\x01\n\x10WarpRegistration\x12\x31\n\x12RequestCertificate\x12\x0b.RegRequest\x1a\x0c.RegResponse\"\x00\x12?\n\x0fRegisterService\x12\x14.ServiceRegistration\x1a\x14.ServiceRegistration\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nwarp.proto\"S\n\x11RemoteMachineInfo\x12\x14\n\x0c\x64isplay_name\x18\x01 \x01(\t\x12\x11\n\tuser_name\x18\x02 \x01(\t\x12\x15\n\rfeature_flags\x18\x03 \x01(\r\"+\n\x13RemoteMachineAvatar\x12\x14\n\x0c\x61vatar_chunk\x18\x01 \x01(\x0c\"/\n\nLookupName\x12\n\n\x02id\x18\x01 \x01(\t\x12\x15\n\rreadable_name\x18\x02 \x01(\t\"\x1e\n\nHaveDuplex\x12\x10\n\x08response\x18\x02 \x01(\x08\"\x19\n\x08VoidType\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\"Z\n\x06OpInfo\x12\r\n\x05ident\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x15\n\rreadable_name\x18\x03 \x01(\t\x12\x17\n\x0fuse_compression\x18\x04 \x01(\x08\"0\n\x08StopInfo\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\r\n\x05\x65rror\x18\x02 \x01(\x08\"\xd0\x01\n\x11TransferOpRequest\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\x13\n\x0bsender_name\x18\x02 \x01(\t\x12\x15\n\rreceiver_name\x18\x03 \x01(\t\x12\x10\n\x08receiver\x18\x04 \x01(\t\x12\x0c\n\x04size\x18\x05 \x01(\x04\x12\r\n\x05\x63ount\x18\x06 \x01(\x04\x12\x16\n\x0ename_if_single\x18\x07 \x01(\t\x12\x16\n\x0emime_if_single\x18\x08 \x01(\t\x12\x19\n\x11top_dir_basenames\x18\t \x03(\t\"\x88\x01\n\tFileChunk\x12\x15\n\rrelative_path\x18\x01 \x01(\t\x12\x11\n\tfile_type\x18\x02 \x01(\x05\x12\x16\n\x0esymlink_target\x18\x03 \x01(\t\x12\r\n\x05\x63hunk\x18\x04 \x01(\x0c\x12\x11\n\tfile_mode\x18\x05 \x01(\r\x12\x17\n\x04time\x18\x06 \x01(\x0b\x32\t.FileTime\"-\n\x08\x46ileTime\x12\r\n\x05mtime\x18\x01 \x01(\x04\x12\x12\n\nmtime_usec\x18\x02 \x01(\r\"8\n\nRegRequest\x12\n\n\x02ip\x18\x01 \x01(\t\x12\x10\n\x08hostname\x18\x02 \x01(\t\x12\x0c\n\x04ipv6\x18\x03 \x01(\t\"\"\n\x0bRegResponse\x12\x13\n\x0blocked_cert\x18\x01 \x01(\t\"\x8b\x01\n\x13ServiceRegistration\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\x12\x0c\n\x04port\x18\x03 \x01(\r\x12\x10\n\x08hostname\x18\x04 \x01(\t\x12\x13\n\x0b\x61pi_version\x18\x05 \x01(\r\x12\x11\n\tauth_port\x18\x06 \x01(\r\x12\x0c\n\x04ipv6\x18\x07 \x01(\t\"@\n\x0bTextMessage\x12\r\n\x05ident\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x0f\n\x07message\x18\x03 \x01(\t2\xa0\x04\n\x04Warp\x12\x33\n\x15\x43heckDuplexConnection\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12.\n\x10WaitingForDuplex\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12\x39\n\x14GetRemoteMachineInfo\x12\x0b.LookupName\x1a\x12.RemoteMachineInfo\"\x00\x12?\n\x16GetRemoteMachineAvatar\x12\x0b.LookupName\x1a\x14.RemoteMachineAvatar\"\x00\x30\x01\x12;\n\x18ProcessTransferOpRequest\x12\x12.TransferOpRequest\x1a\t.VoidType\"\x00\x12\'\n\x0fPauseTransferOp\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12,\n\x0fSendTextMessage\x12\x0c.TextMessage\x1a\t.VoidType\"\x00\x12(\n\rStartTransfer\x12\x07.OpInfo\x1a\n.FileChunk\"\x00\x30\x01\x12/\n\x17\x43\x61ncelTransferOpRequest\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12&\n\x0cStopTransfer\x12\t.StopInfo\x1a\t.VoidType\"\x00\x12 \n\x04Ping\x12\x0b.LookupName\x1a\t.VoidType\"\x00\x32\x86\x01\n\x10WarpRegistration\x12\x31\n\x12RequestCertificate\x12\x0b.RegRequest\x1a\x0c.RegResponse\"\x00\x12?\n\x0fRegisterService\x12\x14.ServiceRegistration\x1a\x14.ServiceRegistration\"\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -32,35 +32,35 @@ if not _descriptor._USE_C_DESCRIPTORS: DESCRIPTOR._loaded_options = None _globals['_REMOTEMACHINEINFO']._serialized_start=14 - _globals['_REMOTEMACHINEINFO']._serialized_end=74 - _globals['_REMOTEMACHINEAVATAR']._serialized_start=76 - _globals['_REMOTEMACHINEAVATAR']._serialized_end=119 - _globals['_LOOKUPNAME']._serialized_start=121 - _globals['_LOOKUPNAME']._serialized_end=168 - _globals['_HAVEDUPLEX']._serialized_start=170 - _globals['_HAVEDUPLEX']._serialized_end=200 - _globals['_VOIDTYPE']._serialized_start=202 - _globals['_VOIDTYPE']._serialized_end=227 - _globals['_OPINFO']._serialized_start=229 - _globals['_OPINFO']._serialized_end=319 - _globals['_STOPINFO']._serialized_start=321 - _globals['_STOPINFO']._serialized_end=369 - _globals['_TRANSFEROPREQUEST']._serialized_start=372 - _globals['_TRANSFEROPREQUEST']._serialized_end=580 - _globals['_FILECHUNK']._serialized_start=583 - _globals['_FILECHUNK']._serialized_end=719 - _globals['_FILETIME']._serialized_start=721 - _globals['_FILETIME']._serialized_end=766 - _globals['_REGREQUEST']._serialized_start=768 - _globals['_REGREQUEST']._serialized_end=824 - _globals['_REGRESPONSE']._serialized_start=826 - _globals['_REGRESPONSE']._serialized_end=860 - _globals['_SERVICEREGISTRATION']._serialized_start=863 - _globals['_SERVICEREGISTRATION']._serialized_end=1002 - _globals['_TEXTMESSAGE']._serialized_start=1004 - _globals['_TEXTMESSAGE']._serialized_end=1068 - _globals['_WARP']._serialized_start=1071 - _globals['_WARP']._serialized_end=1615 - _globals['_WARPREGISTRATION']._serialized_start=1618 - _globals['_WARPREGISTRATION']._serialized_end=1752 + _globals['_REMOTEMACHINEINFO']._serialized_end=97 + _globals['_REMOTEMACHINEAVATAR']._serialized_start=99 + _globals['_REMOTEMACHINEAVATAR']._serialized_end=142 + _globals['_LOOKUPNAME']._serialized_start=144 + _globals['_LOOKUPNAME']._serialized_end=191 + _globals['_HAVEDUPLEX']._serialized_start=193 + _globals['_HAVEDUPLEX']._serialized_end=223 + _globals['_VOIDTYPE']._serialized_start=225 + _globals['_VOIDTYPE']._serialized_end=250 + _globals['_OPINFO']._serialized_start=252 + _globals['_OPINFO']._serialized_end=342 + _globals['_STOPINFO']._serialized_start=344 + _globals['_STOPINFO']._serialized_end=392 + _globals['_TRANSFEROPREQUEST']._serialized_start=395 + _globals['_TRANSFEROPREQUEST']._serialized_end=603 + _globals['_FILECHUNK']._serialized_start=606 + _globals['_FILECHUNK']._serialized_end=742 + _globals['_FILETIME']._serialized_start=744 + _globals['_FILETIME']._serialized_end=789 + _globals['_REGREQUEST']._serialized_start=791 + _globals['_REGREQUEST']._serialized_end=847 + _globals['_REGRESPONSE']._serialized_start=849 + _globals['_REGRESPONSE']._serialized_end=883 + _globals['_SERVICEREGISTRATION']._serialized_start=886 + _globals['_SERVICEREGISTRATION']._serialized_end=1025 + _globals['_TEXTMESSAGE']._serialized_start=1027 + _globals['_TEXTMESSAGE']._serialized_end=1091 + _globals['_WARP']._serialized_start=1094 + _globals['_WARP']._serialized_end=1638 + _globals['_WARPREGISTRATION']._serialized_start=1641 + _globals['_WARPREGISTRATION']._serialized_end=1775 # @@protoc_insertion_point(module_scope) diff --git a/src/warpinator.py b/src/warpinator.py index c9eae5894..fba7811de 100644 --- a/src/warpinator.py +++ b/src/warpinator.py @@ -526,6 +526,7 @@ def __init__(self): self.user_send_msg_button.connect("clicked", self.send_msg_button_clicked) self.user_msg_entry = self.builder.get_object("user_msg_entry") self.user_msg_entry.connect("key-press-event", self.msg_entry_key_press) + self.user_msg_box = self.builder.get_object("user_msg_box") self.user_online_box = self.builder.get_object("user_online_box") self.user_online_image = self.builder.get_object("user_online_image") self.user_online_label = self.builder.get_object("user_online_label") @@ -1079,6 +1080,8 @@ def refresh_remote_machine_view(self): else: self.user_avatar_image.set_from_icon_name("xsi-avatar-default-symbolic", Gtk.IconSize.DND) + self.user_msg_box.set_visible(remote.supports_messages) + self.add_op_items() self.sync_favorite() From 13f1ff4b13e6f9313b8349c5681cbab31d3f25be Mon Sep 17 00:00:00 2001 From: slowscript Date: Wed, 19 Nov 2025 16:25:26 +0100 Subject: [PATCH 5/6] Use wider message label for TextMessage op --- resources/op-item.ui | 26 ++++++++++++++++++++++++++ src/warpinator.py | 41 +++++++++++++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/resources/op-item.ui b/resources/op-item.ui index 039823ac4..c0ef17337 100644 --- a/resources/op-item.ui +++ b/resources/op-item.ui @@ -236,6 +236,32 @@ 2 + + + True + False + vertical + + + True + False + True + True + word-char + 80 + 0 + + + True + True + 0 + + + + + text-message + + True diff --git a/src/warpinator.py b/src/warpinator.py index fba7811de..e2cf509bd 100644 --- a/src/warpinator.py +++ b/src/warpinator.py @@ -104,6 +104,7 @@ def __init__(self, op): self.op_status_stack = self.builder.get_object("op_status_stack") self.op_transfer_status_message = self.builder.get_object("op_transfer_status_message") self.op_transfer_problem_label = self.builder.get_object("op_transfer_problem_label") + self.op_transfer_text_message = self.builder.get_object("op_transfer_text_message") self.op_progress_bar = self.builder.get_object("op_transfer_progress_bar") self.accept_button = self.builder.get_object("transfer_accept") self.decline_button = self.builder.get_object("transfer_decline") @@ -186,11 +187,29 @@ def refresh_status_widgets(self): self.op_transfer_problem_label.set_text(_("Some files not found")) elif self.op.status == OpStatus.FINISHED: if isinstance(self.op, TextMessageOp): - msg = "\n".join(self.op.message.split("\n")[:4]) # Max 4 lines - if len(msg) > 120: # Max 120 chars (4*30 per line) -- FIXME: This might still exceed 4 lines - msg = msg[:117] + "..." - self.op_transfer_status_message.set_text(msg) - self.op_transfer_status_message.set_selectable(True) + label_length = 80 + lines_left = 4 + lines = self.op.message.split("\n") + msg = "" + for l in lines: + wrapped_lines = math.ceil(len(l) / label_length) + if wrapped_lines > lines_left: + msg += "\n" + l[:label_length*lines_left-3] + "..." + break + else: + msg += "\n" + l + lines_left -= wrapped_lines + if lines_left < 1: + last_line_len = len(l) % label_length + if last_line_len == 0 and len(l) > 0: + last_line_len = label_length + if len(lines) > 4: + if last_line_len > label_length-3: + msg = msg[:-last_line_len+label_length-3] + msg += "..." + break + msg = msg[1:] # skip first \n + self.op_transfer_text_message.set_text(msg) else: self.op_transfer_status_message.set_text(_("Completed")) elif self.op.status == OpStatus.FINISHED_WARNING: @@ -245,13 +264,15 @@ def refresh_buttons_and_icons(self): self.set_visible_buttons(TRANSFER_FILE_NOT_FOUND_BUTTONS) elif self.op.status in (OpStatus.FINISHED, OpStatus.FINISHED_WARNING): - self.op_status_stack.set_visible_child_name("message") - if isinstance(self.op, SendOp): - self.set_visible_buttons(TRANSFER_COMPLETED_SENDER_BUTTONS) - elif isinstance(self.op, TextMessageOp): + if isinstance(self.op, TextMessageOp): + self.op_status_stack.set_visible_child_name("text-message") self.set_visible_buttons(TRANSFER_TEXT_MESSAGE_BUTTONS) else: - self.set_visible_buttons(TRANSFER_COMPLETED_RECEIVER_BUTTONS) + self.op_status_stack.set_visible_child_name("message") + if isinstance(self.op, SendOp): + self.set_visible_buttons(TRANSFER_COMPLETED_SENDER_BUTTONS) + else: + self.set_visible_buttons(TRANSFER_COMPLETED_RECEIVER_BUTTONS) elif self.op.status in (OpStatus.CANCELLED_PERMISSION_BY_SENDER, OpStatus.CANCELLED_PERMISSION_BY_RECEIVER): self.set_visible_buttons(TRANSFER_CANCELLED_BUTTONS) From 037f53048811b57cc72abe499769dbc4643dada9 Mon Sep 17 00:00:00 2001 From: slowscript Date: Wed, 19 Nov 2025 16:56:55 +0100 Subject: [PATCH 6/6] Hide unused columns for text message, highlight remote on new message --- src/remote.py | 2 +- src/warpinator.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/remote.py b/src/remote.py index f7123802b..8ec2302e8 100644 --- a/src/remote.py +++ b/src/remote.py @@ -618,7 +618,7 @@ def add_op(self, op): if isinstance(op, SendOp): op.connect("initial-setup-complete", self.notify_remote_machine_of_new_op) self.emit("new-outgoing-op", op) - if isinstance(op, ReceiveOp): + if isinstance(op, (ReceiveOp, TextMessageOp)): self.emit("new-incoming-op", op) def set_busy(): diff --git a/src/warpinator.py b/src/warpinator.py index e2cf509bd..ae612cea7 100644 --- a/src/warpinator.py +++ b/src/warpinator.py @@ -227,7 +227,9 @@ def refresh_buttons_and_icons(self): self.mime_image.set_from_gicon(self.op.gicon, Gtk.IconSize.BUTTON) self.transfer_size_label.set_text(self.op.size_string) + self.transfer_size_label.set_visible(not isinstance(self.op, TextMessageOp)) self.transfer_description_label.set_text(self.op.description) + self.transfer_description_label.set_visible(not isinstance(self.op, TextMessageOp)) if self.op.status in (OpStatus.INIT, OpStatus.CALCULATING): self.op_status_stack.set_visible_child_name("calculating")