Skip to content

Commit b539df2

Browse files
authored
Ability to send text messages (#225)
* Receiving text messages Display text message as a transfer in UI, show notification, allow copy text * Sending text messages Send message button and dialog * Enter message inside main window instead of a dialog * Only allow sending messages when remote supports them * Use wider message label for TextMessage op * Hide unused columns for text message, highlight remote on new message
1 parent 741a6b1 commit b539df2

File tree

11 files changed

+363
-53
lines changed

11 files changed

+363
-53
lines changed

resources/main-window.ui

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
<property name="can-focus">False</property>
1919
<property name="icon-name">xsi-window-close-symbolic</property>
2020
</object>
21+
<object class="GtkImage" id="image4">
22+
<property name="visible">True</property>
23+
<property name="can-focus">False</property>
24+
<property name="icon-name">xsi-mail-send</property>
25+
</object>
2126
<object class="GtkImage" id="user_favorite_image">
2227
<property name="visible">True</property>
2328
<property name="can-focus">False</property>
@@ -700,9 +705,51 @@
700705
<packing>
701706
<property name="expand">False</property>
702707
<property name="fill">True</property>
703-
<property name="position">2</property>
708+
<property name="position">1</property>
704709
</packing>
705710
</child>
711+
<child>
712+
<object class="GtkBox" id="user_msg_box">
713+
<property name="visible">True</property>
714+
<property name="can-focus">False</property>
715+
<property name="spacing">6</property>
716+
<child>
717+
<object class="GtkScrolledWindow">
718+
<property name="height-request">40</property>
719+
<property name="visible">True</property>
720+
<property name="can-focus">True</property>
721+
<property name="hscrollbar-policy">never</property>
722+
<property name="vscrollbar-policy">external</property>
723+
<property name="shadow-type">out</property>
724+
<child>
725+
<object class="GtkTextView" id="user_msg_entry">
726+
<property name="visible">True</property>
727+
<property name="can-focus">True</property>
728+
<property name="wrap-mode">word-char</property>
729+
</object>
730+
</child>
731+
</object>
732+
<packing>
733+
<property name="expand">True</property>
734+
<property name="fill">True</property>
735+
<property name="position">0</property>
736+
</packing>
737+
</child>
738+
<child>
739+
<object class="GtkButton" id="user_send_msg_button">
740+
<property name="visible">True</property>
741+
<property name="can-focus">True</property>
742+
<property name="receives-default">False</property>
743+
<property name="image">image4</property>
744+
</object>
745+
<packing>
746+
<property name="expand">False</property>
747+
<property name="fill">True</property>
748+
<property name="position">1</property>
749+
</packing>
750+
</child>
751+
</object>
752+
</child>
706753
</object>
707754
<packing>
708755
<property name="expand">False</property>

resources/op-item.ui

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@
5151
<property name="halign">center</property>
5252
<property name="icon_name">xsi-list-remove-symbolic</property>
5353
</object>
54+
<object class="GtkImage" id="image11">
55+
<property name="visible">True</property>
56+
<property name="can_focus">False</property>
57+
<property name="halign">center</property>
58+
<property name="icon_name">xsi-edit-copy-symbolic</property>
59+
</object>
5460
<object class="GtkBox" id="op_item">
5561
<property name="visible">True</property>
5662
<property name="can_focus">False</property>
@@ -230,6 +236,32 @@
230236
<property name="position">2</property>
231237
</packing>
232238
</child>
239+
<child>
240+
<object class="GtkBox">
241+
<property name="visible">True</property>
242+
<property name="can_focus">False</property>
243+
<property name="orientation">vertical</property>
244+
<child>
245+
<object class="GtkLabel" id="op_transfer_text_message">
246+
<property name="visible">True</property>
247+
<property name="can-focus">False</property>
248+
<property name="selectable">True</property>
249+
<property name="wrap">True</property>
250+
<property name="wrap-mode">word-char</property>
251+
<property name="max_width_chars">80</property>
252+
<property name="xalign">0</property>
253+
</object>
254+
<packing>
255+
<property name="expand">True</property>
256+
<property name="fill">True</property>
257+
<property name="position">0</property>
258+
</packing>
259+
</child>
260+
</object>
261+
<packing>
262+
<property name="name">text-message</property>
263+
</packing>
264+
</child>
233265
</object>
234266
<packing>
235267
<property name="expand">True</property>
@@ -386,6 +418,21 @@
386418
<property name="position">8</property>
387419
</packing>
388420
</child>
421+
<child>
422+
<object class="GtkButton" id="transfer_copy_message">
423+
<property name="visible">True</property>
424+
<property name="can_focus">True</property>
425+
<property name="receives_default">True</property>
426+
<property name="tooltip_text" translatable="yes">Copy message</property>
427+
<property name="valign">center</property>
428+
<property name="image">image11</property>
429+
</object>
430+
<packing>
431+
<property name="expand">False</property>
432+
<property name="fill">False</property>
433+
<property name="position">9</property>
434+
</packing>
435+
</child>
389436
<style>
390437
<class name="linked"/>
391438
</style>

src/notifications.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,34 @@ def _notification_response(self, action, variant, op):
224224

225225
app = Gio.Application.get_default()
226226
app.lookup_action("notification-response").disconnect_by_func(self._notification_response)
227+
228+
class TextMessageNotification():
229+
def __init__(self, op):
230+
self.op = op
231+
self.send_notification()
232+
233+
@misc._idle
234+
def send_notification(self):
235+
if prefs.get_show_notifications():
236+
notification = Gio.Notification.new(_("New message from %s") % self.op.sender_name)
237+
notification.set_body(self.op.message)
238+
notification.set_icon(Gio.ThemedIcon(name="org.x.Warpinator-symbolic"))
239+
notification.set_priority(Gio.NotificationPriority.URGENT)
240+
241+
notification.add_button(_("Copy"), "app.notification-response::copy")
242+
notification.set_default_action("app.notification-response::focus")
243+
244+
app = Gio.Application.get_default()
245+
app.lookup_action("notification-response").connect("activate", self._notification_response, self.op)
246+
app.send_notification(self.op.sender, notification)
247+
248+
def _notification_response(self, action, variant, op):
249+
response = variant.unpack()
250+
251+
if response == "copy":
252+
op.copy_message()
253+
else:
254+
op.focus()
255+
256+
app = Gio.Application.get_default()
257+
app.lookup_action("notification-response").disconnect_by_func(self._notification_response)

src/ops.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import logging
55
from pathlib import Path
66

7-
from gi.repository import GObject, GLib, Gio
7+
from gi.repository import GObject, GLib, Gio, Gtk, Gdk
88

99
import grpc
1010

@@ -283,3 +283,23 @@ def stop_transfer(self):
283283
def remove_transfer(self):
284284
self.emit("op-command", OpCommand.REMOVE_TRANSFER)
285285

286+
class TextMessageOp(CommonOp):
287+
message = None
288+
289+
def __init__(self, direction, sender):
290+
super(TextMessageOp, self).__init__(direction, sender)
291+
self.gicon = Gio.ThemedIcon.new("xsi-mail-message-new-symbolic")
292+
self.description = _("Text message")
293+
294+
def copy_message(self):
295+
cb = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
296+
cb.set_text(self.message, -1)
297+
298+
def send_notification(self):
299+
notifications.TextMessageNotification(self)
300+
301+
def remove_transfer(self):
302+
self.emit("op-command", OpCommand.REMOVE_TRANSFER)
303+
304+
def retry_transfer(self):
305+
self.emit("op-command", OpCommand.RETRY_TRANSFER)

src/remote.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
import misc
1919
import transfers
2020
import auth
21-
from ops import SendOp, ReceiveOp
22-
from util import TransferDirection, OpStatus, OpCommand, RemoteStatus, ReceiveError
21+
from ops import SendOp, ReceiveOp, TextMessageOp
22+
from util import TransferDirection, OpStatus, OpCommand, RemoteStatus, ReceiveError, RemoteFeatures
2323

2424
_ = gettext.gettext
2525

@@ -56,6 +56,7 @@ def __init__(self, ident, hostname, display_hostname, ip_info, port, local_ident
5656
self.display_name = ""
5757
self.favorite = prefs.get_is_favorite(self.ident)
5858
self.recent_time = 0 # Keep monotonic time when visited on the user page
59+
self.supports_messages = False
5960

6061
self.avatar_surface = None
6162
self.transfer_ops = []
@@ -366,6 +367,8 @@ def get_info_finished(future):
366367
info = future.result()
367368
self.display_name = info.display_name
368369
self.user_name = info.user_name
370+
feature_flags = RemoteFeatures(info.feature_flags)
371+
self.supports_messages = RemoteFeatures.TEXT_MESSAGES in feature_flags
369372
self.favorite = prefs.get_is_favorite(self.ident)
370373

371374
valid = GLib.utf8_make_valid(self.display_name, -1)
@@ -590,6 +593,21 @@ def _send_files(uri_list):
590593
util.add_to_recents_if_single_selection(uri_list)
591594
self.rpc_call(_send_files, uri_list)
592595

596+
def send_text_message(self, message):
597+
op = TextMessageOp(TransferDirection.TO_REMOTE_MACHINE, self.local_ident)
598+
op.message = message
599+
op.status = OpStatus.FINISHED
600+
self.add_op(op)
601+
self.rpc_call(self.do_send_text_message, op)
602+
603+
def do_send_text_message(self, op):
604+
try:
605+
self.stub.SendTextMessage(warp_pb2.TextMessage(ident=self.local_ident, timestamp=op.start_time, message=op.message))
606+
except Exception as e:
607+
logging.error("Sending message failed: %s" % e)
608+
op.status = OpStatus.FAILED
609+
op.emit_status_changed()
610+
593611
@misc._idle
594612
def add_op(self, op):
595613
if op not in self.transfer_ops:
@@ -600,7 +618,7 @@ def add_op(self, op):
600618
if isinstance(op, SendOp):
601619
op.connect("initial-setup-complete", self.notify_remote_machine_of_new_op)
602620
self.emit("new-outgoing-op", op)
603-
if isinstance(op, ReceiveOp):
621+
if isinstance(op, (ReceiveOp, TextMessageOp)):
604622
self.emit("new-incoming-op", op)
605623

606624
def set_busy():
@@ -662,8 +680,13 @@ def op_command_issued(self, op, command):
662680
elif command == OpCommand.STOP_TRANSFER_BY_SENDER:
663681
self.rpc_call(self.stop_transfer_op, op, by_sender=True)
664682
elif command == OpCommand.RETRY_TRANSFER:
665-
op.set_status(OpStatus.WAITING_PERMISSION)
666-
self.rpc_call(self.send_transfer_op_request, op)
683+
if isinstance(op, TextMessageOp):
684+
op.status = OpStatus.FINISHED
685+
op.emit_status_changed()
686+
self.rpc_call(self.do_send_text_message, op)
687+
else:
688+
op.set_status(OpStatus.WAITING_PERMISSION)
689+
self.rpc_call(self.send_transfer_op_request, op)
667690
elif command == OpCommand.REMOVE_TRANSFER:
668691
self.remove_op(op)
669692
# receive

src/server.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
import util
3030
import misc
3131
import transfers
32-
from ops import ReceiveOp
33-
from util import TransferDirection, OpStatus, RemoteStatus
32+
from ops import ReceiveOp, TextMessageOp
33+
from util import TransferDirection, OpStatus, RemoteStatus, RemoteFeatures
3434

3535
import zeroconf
3636
from zeroconf import ServiceInfo, Zeroconf, ServiceBrowser, IPVersion
@@ -41,6 +41,8 @@
4141

4242
SERVICE_TYPE = "_warpinator._tcp.local."
4343

44+
SERVER_FEATURES = RemoteFeatures.TEXT_MESSAGES
45+
4446
# server (this is on a separate thread from the ui, grpc isn't compatible with
4547
# gmainloop)
4648
class Server(threading.Thread, warp_pb2_grpc.WarpServicer, GObject.Object):
@@ -562,7 +564,8 @@ def GetRemoteMachineInfo(self, request, context):
562564
logging.debug("Server RPC: GetRemoteMachineInfo from '%s'" % request.readable_name)
563565

564566
return warp_pb2.RemoteMachineInfo(display_name=GLib.get_real_name(),
565-
user_name=GLib.get_user_name())
567+
user_name=GLib.get_user_name(),
568+
feature_flags=SERVER_FEATURES)
566569

567570
def GetRemoteMachineAvatar(self, request, context):
568571
logging.debug("Server RPC: GetRemoteMachineAvatar from '%s'" % request.readable_name)
@@ -715,3 +718,20 @@ def StopTransfer(self, request, context):
715718
op.set_status(OpStatus.FAILED)
716719

717720
return void
721+
722+
def SendTextMessage(self, request, context):
723+
logging.debug("Server RPC: SendTextMessage from '%s'" % request.ident)
724+
try:
725+
remote_machine:remote.RemoteMachine = self.remote_machines[request.ident]
726+
except KeyError as e:
727+
logging.warning("Received text message from unknown remote: %s" % e)
728+
return
729+
730+
op = TextMessageOp(TransferDirection.FROM_REMOTE_MACHINE, request.ident)
731+
op.sender_name = remote_machine.display_name
732+
op.message = request.message
733+
op.status = OpStatus.FINISHED
734+
remote_machine.add_op(op)
735+
op.send_notification()
736+
737+
return void

src/util.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def shutdown(self, wait=True):
141141
self._factory_thread.join()
142142
logging.debug("NewThreadExecutor: Shutdown complete")
143143

144-
from enum import IntEnum
144+
from enum import IntEnum, IntFlag
145145
TransferDirection = IntEnum('TransferDirection', 'TO_REMOTE_MACHINE \
146146
FROM_REMOTE_MACHINE')
147147

@@ -192,6 +192,9 @@ def shutdown(self, wait=True):
192192
CERT_UP_TO_DATE \
193193
FAILURE')
194194

195+
class RemoteFeatures(IntFlag):
196+
TEXT_MESSAGES = 1 << 0
197+
195198
class ReceiveError(Exception):
196199
def __init__(self, message, fatal=True):
197200
self.fatal = fatal

src/warp.proto

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ service Warp {
1818
rpc GetRemoteMachineAvatar(LookupName) returns (stream RemoteMachineAvatar) {}
1919
rpc ProcessTransferOpRequest(TransferOpRequest) returns (VoidType) {}
2020
rpc PauseTransferOp(OpInfo) returns (VoidType) {}
21+
rpc SendTextMessage(TextMessage) returns (VoidType) {}
2122

2223
// Receiver methods
2324
rpc StartTransfer(OpInfo) returns (stream FileChunk) {}
@@ -32,6 +33,7 @@ service Warp {
3233
message RemoteMachineInfo {
3334
string display_name = 1;
3435
string user_name = 2;
36+
uint32 feature_flags = 3;
3537
}
3638

3739
message RemoteMachineAvatar {
@@ -114,3 +116,8 @@ message ServiceRegistration {
114116
string ipv6 = 7;
115117
}
116118

119+
message TextMessage {
120+
string ident = 1;
121+
uint64 timestamp = 2;
122+
string message = 3;
123+
}

0 commit comments

Comments
 (0)