Added image and file attachment sending

This commit is contained in:
Mark Qvist 2024-03-16 22:59:19 +01:00
parent 8f8c1ec68f
commit 7f891d68e0
2 changed files with 269 additions and 48 deletions

View File

@ -24,6 +24,8 @@ import RNS.vendor.umsgpack as msgpack
if not args.daemon: if not args.daemon:
import plyer import plyer
from kivy.logger import Logger, LOG_LEVELS from kivy.logger import Logger, LOG_LEVELS
from PIL import Image as PilImage
import io
# Squelch excessive method signature logging # Squelch excessive method signature logging
class redirect_log(): class redirect_log():
@ -67,7 +69,7 @@ if args.daemon:
StringProperty = DaemonElement; BaseButton = DaemonElement; MDIconButton = DaemonElement; MDFileManager = DaemonElement; StringProperty = DaemonElement; BaseButton = DaemonElement; MDIconButton = DaemonElement; MDFileManager = DaemonElement;
toast = DaemonElement; dp = DaemonElement; sp = DaemonElement; MDRectangleFlatButton = DaemonElement; MDDialog = DaemonElement; toast = DaemonElement; dp = DaemonElement; sp = DaemonElement; MDRectangleFlatButton = DaemonElement; MDDialog = DaemonElement;
colors = DaemonElement; Telemeter = DaemonElement; CustomMapMarker = DaemonElement; MBTilesMapSource = DaemonElement; colors = DaemonElement; Telemeter = DaemonElement; CustomMapMarker = DaemonElement; MBTilesMapSource = DaemonElement;
MapSource = DaemonElement; webbrowser = DaemonElement; Conversations = DaemonElement; MsgSync = DaemonElement; MapSource = DaemonElement; webbrowser = DaemonElement; Conversations = DaemonElement; MsgSync = DaemonElement; IconLeftWidget = DaemonElement;
NewConv = DaemonElement; Telemetry = DaemonElement; ObjectDetails = DaemonElement; Announces = DaemonElement; NewConv = DaemonElement; Telemetry = DaemonElement; ObjectDetails = DaemonElement; Announces = DaemonElement;
Messages = DaemonElement; ts_format = DaemonElement; messages_screen_kv = DaemonElement; plyer = DaemonElement; multilingual_markup = DaemonElement; Messages = DaemonElement; ts_format = DaemonElement; messages_screen_kv = DaemonElement; plyer = DaemonElement; multilingual_markup = DaemonElement;
ContentNavigationDrawer = DaemonElement; DrawerList = DaemonElement; IconListItem = DaemonElement; escape_markup = DaemonElement; ContentNavigationDrawer = DaemonElement; DrawerList = DaemonElement; IconListItem = DaemonElement; escape_markup = DaemonElement;
@ -83,11 +85,10 @@ else:
from kivy.effects.scroll import ScrollEffect from kivy.effects.scroll import ScrollEffect
from kivy.uix.screenmanager import ScreenManager from kivy.uix.screenmanager import ScreenManager
from kivy.uix.screenmanager import FadeTransition, NoTransition, SlideTransition from kivy.uix.screenmanager import FadeTransition, NoTransition, SlideTransition
from kivymd.uix.list import OneLineIconListItem from kivymd.uix.list import OneLineIconListItem, IconLeftWidget
from kivy.properties import StringProperty from kivy.properties import StringProperty
from kivymd.uix.button import BaseButton, MDIconButton from kivymd.uix.button import BaseButton, MDIconButton
from kivymd.uix.filemanager import MDFileManager from kivymd.uix.filemanager import MDFileManager
from kivymd.toast import toast
from kivy.metrics import dp, sp from kivy.metrics import dp, sp
from kivymd.uix.button import MDRectangleFlatButton from kivymd.uix.button import MDRectangleFlatButton
from kivymd.uix.dialog import MDDialog from kivymd.uix.dialog import MDDialog
@ -112,6 +113,7 @@ else:
from ui.messages import Messages, ts_format, messages_screen_kv from ui.messages import Messages, ts_format, messages_screen_kv
from ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem from ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem
from ui.helpers import multilingual_markup from ui.helpers import multilingual_markup
from kivymd.toast import toast
from jnius import cast from jnius import cast
from jnius import autoclass from jnius import autoclass
@ -134,6 +136,10 @@ else:
from .ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem from .ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem
from .ui.helpers import multilingual_markup from .ui.helpers import multilingual_markup
class toast:
def __init__(self, *kwargs):
pass
from kivy.config import Config from kivy.config import Config
Config.set('input', 'mouse', 'mouse,disable_multitouch') Config.set('input', 'mouse', 'mouse,disable_multitouch')
@ -209,6 +215,9 @@ class SidebandApp(MDApp):
self.final_load_completed = False self.final_load_completed = False
self.service_last_available = 0 self.service_last_available = 0
self.attach_path = None
self.attach_type = None
Window.softinput_mode = "below_target" Window.softinput_mode = "below_target"
self.icon = self.sideband.asset_dir+"/icon.png" self.icon = self.sideband.asset_dir+"/icon.png"
self.notification_icon = self.sideband.asset_dir+"/notification_icon.png" self.notification_icon = self.sideband.asset_dir+"/notification_icon.png"
@ -1155,6 +1164,67 @@ class SidebandApp(MDApp):
else: else:
msg_content = self.messages_view.ids.message_text.text msg_content = self.messages_view.ids.message_text.text
context_dest = self.messages_view.ids.messages_scrollview.active_conversation context_dest = self.messages_view.ids.messages_scrollview.active_conversation
attachment = None
image = None
if not self.outbound_mode_command and not self.outbound_mode_paper:
if self.attach_type != None and self.attach_path != None:
try:
RNS.log("Processing "+str(self.attach_type)+" attachment \""+str(self.attach_path)+"\"", RNS.LOG_DEBUG)
fbn = os.path.basename(self.attach_path)
if self.attach_type == "file":
with open(self.attach_path, "rb") as af:
attachment = [fbn, af.read()]
elif self.attach_type == "lbimg":
max_size = 320, 320
with PilImage.open(self.attach_path) as im:
im.thumbnail(max_size)
buf = io.BytesIO()
im.save(buf, format="webp", quality=22)
image = ["webp", buf.getvalue()]
elif self.attach_type == "defimg":
max_size = 640, 640
with PilImage.open(self.attach_path) as im:
im.thumbnail(max_size)
buf = io.BytesIO()
im.save(buf, format="webp", quality=66)
image = ["webp", buf.getvalue()]
elif self.attach_type == "hqimg":
max_size = 1280, 1280
with PilImage.open(self.attach_path) as im:
im.thumbnail(max_size)
buf = io.BytesIO()
im.save(buf, format="webp", quality=75)
image = ["webp", buf.getvalue()]
except Exception as e:
self.messages_view.send_error_dialog = MDDialog(
title="Attachment Error",
text="An error occurred while processing the attachment:\n\n[i]"+str(e)+"[/i]",
buttons=[
MDRectangleFlatButton(
text="OK",
font_size=dp(18),
on_release=self.messages_view.close_send_error_dialog
)
],
)
self.messages_view.send_error_dialog.open()
self.attach_type = None
self.attach_path = None
self.update_message_widgets()
RNS.log("Trace:")
RNS.trace_exception(e)
return
self.attach_type = None
self.attach_path = None
self.update_message_widgets()
if self.outbound_mode_command: if self.outbound_mode_command:
if self.sideband.send_command(msg_content, context_dest, False): if self.sideband.send_command(msg_content, context_dest, False):
self.messages_view.ids.message_text.text = "" self.messages_view.ids.message_text.text = ""
@ -1180,7 +1250,7 @@ class SidebandApp(MDApp):
self.messages_view.ids.messages_scrollview.scroll_y = 0 self.messages_view.ids.messages_scrollview.scroll_y = 0
self.jobs(0) self.jobs(0)
elif self.sideband.send_message(msg_content, context_dest, self.outbound_mode_propagation): elif self.sideband.send_message(msg_content, context_dest, self.outbound_mode_propagation, attachment = attachment, image = image):
self.messages_view.ids.message_text.text = "" self.messages_view.ids.message_text.text = ""
self.messages_view.ids.messages_scrollview.scroll_y = 0 self.messages_view.ids.messages_scrollview.scroll_y = 0
self.jobs(0) self.jobs(0)
@ -1237,12 +1307,145 @@ class SidebandApp(MDApp):
self.update_message_widgets() self.update_message_widgets()
def message_fm_got_path(self, path):
self.message_fm_exited()
fbn = os.path.basename(path)
try:
tf = open(path, "rb")
tf.close()
self.attach_path = path
if RNS.vendor.platformutils.is_android():
toast("Attached \""+str(fbn)+"\"")
else:
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
ate_dialog = MDDialog(
title="File Attached",
text="The file \""+str(fbn)+"\" was attached, and will be included with the next message sent.",
buttons=[ ok_button ],
)
ok_button.bind(on_release=ate_dialog.dismiss)
ate_dialog.open()
except Exception as e:
RNS.log(f"Error while attaching \"{fbn}\": "+str(e), RNS.LOG_ERROR)
if RNS.vendor.platformutils.get_platform() == "android":
toast("Could not attach \""+str(fbn)+"\"")
else:
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
ate_dialog = MDDialog(
title="Attachment Error",
text="The specified file could not be attached:\n\n[i]"+str(e)+"[/i]",
buttons=[ ok_button ],
)
ok_button.bind(on_release=ate_dialog.dismiss)
ate_dialog.open()
self.update_message_widgets()
def message_fm_exited(self, *args):
self.manager_open = False
self.file_manager.close()
def message_select_file_action(self, sender=None):
perm_ok = False
if RNS.vendor.platformutils.is_android():
perm_ok = self.check_storage_permission()
path = primary_external_storage_path()
else:
perm_ok = True
path = os.path.expanduser("~")
if perm_ok and path != None:
try:
self.file_manager = MDFileManager(
exit_manager=self.message_fm_exited,
select_path=self.message_fm_got_path,
)
# self.file_manager.ext = ["*"]
self.file_manager.show(path)
except Exception as e:
self.sideband.config["map_storage_path"] = None
self.sideband.save_configuration()
toast("Error reading directory, check permissions!")
else:
self.sideband.config["map_storage_path"] = None
self.sideband.save_configuration()
toast("No file access, check permissions!")
def message_attach_action(self, attach_type=None):
self.attach_path = None
self.attach_type = attach_type
self.message_select_file_action()
def message_attachment_action(self, sender): def message_attachment_action(self, sender):
pass if self.attach_path == None:
attach_dialog = None
def a_img_lb(sender):
attach_dialog.dismiss()
self.message_attach_action(attach_type="lbimg")
def a_img_def(sender):
attach_dialog.dismiss()
self.message_attach_action(attach_type="defimg")
def a_img_hq(sender):
attach_dialog.dismiss()
self.message_attach_action(attach_type="hqimg")
def a_file(sender):
attach_dialog.dismiss()
self.message_attach_action(attach_type="file")
ss = int(dp(18))
cancel_button = MDRectangleFlatButton(text="Cancel", font_size=dp(18))
attach_dialog = MDDialog(
title="Add Attachment",
type="simple",
text="Select the type of attachment you want to send with this message\n",
items=[
DialogItem(IconLeftWidget(icon="message-image-outline"), text="[size="+str(ss)+"]Low-bandwidth Image[/size]", on_release=a_img_lb),
DialogItem(IconLeftWidget(icon="file-image"), text="[size="+str(ss)+"]Medium Image[/size]", on_release=a_img_def),
DialogItem(IconLeftWidget(icon="image-outline"), text="[size="+str(ss)+"]High-res Image[/size]", on_release=a_img_hq),
DialogItem(IconLeftWidget(icon="file-outline"), text="[size="+str(ss)+"]File Attachment[/size]", on_release=a_file),
],
buttons=[ cancel_button ],
width_offset=dp(12),
)
cancel_button.bind(on_release=attach_dialog.dismiss)
attach_dialog.open()
attach_dialog.update_width()
else:
self.attach_path = None
self.attach_type = None
self.update_message_widgets()
if RNS.vendor.platformutils.get_platform() == "android":
toast("Attachment removed")
else:
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
ate_dialog = MDDialog(
title="Attachment Removed",
text="The attached resource was removed from the message",
buttons=[ ok_button ],
)
ok_button.bind(on_release=ate_dialog.dismiss)
ate_dialog.open()
def update_message_widgets(self): def update_message_widgets(self):
toolbar_items = self.messages_view.ids.messages_toolbar.ids.right_actions.children toolbar_items = self.messages_view.ids.messages_toolbar.ids.right_actions.children
mode_item = toolbar_items[1] mode_item = toolbar_items[1]
attachment_item = toolbar_items[4]
if self.attach_path != None:
attachment_item.icon = "attachment-check"
else:
attachment_item.icon = "attachment-plus"
if self.outbound_mode_paper: if self.outbound_mode_paper:
mode_item.icon = "qrcode" mode_item.icon = "qrcode"
@ -3719,7 +3922,17 @@ class SidebandApp(MDApp):
toast("Using \""+os.path.basename(path)+"\" as offline map") toast("Using \""+os.path.basename(path)+"\" as offline map")
except Exception as e: except Exception as e:
RNS.log(f"Error while loading map \"{path}\": "+str(e), RNS.LOG_ERROR) RNS.log(f"Error while loading map \"{path}\": "+str(e), RNS.LOG_ERROR)
if RNS.vendor.platformutils.get_platform() == "android":
toast("Could not load map \""+os.path.basename(path)+"\"") toast("Could not load map \""+os.path.basename(path)+"\"")
else:
ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18))
map_dialog = MDDialog(
title="Map Error",
text="The specified map file could not be loaded. Make sure the selected file is an MBTiles map in raster format. Vector maps are currently not supported.",
buttons=[ ok_button ],
)
ok_button.bind(on_release=map_dialog.dismiss)
map_dialog.open()
self.sideband.config["map_storage_file"] = None self.sideband.config["map_storage_file"] = None
self.sideband.config["map_use_offline"] = False self.sideband.config["map_use_offline"] = False
self.sideband.config["map_use_online"] = True self.sideband.config["map_use_online"] = True
@ -3731,6 +3944,47 @@ class SidebandApp(MDApp):
self.file_manager.close() self.file_manager.close()
self.map_update_source() self.map_update_source()
def map_select_file_action(self, sender=None):
perm_ok = False
if self.sideband.config["map_storage_path"] == None:
if RNS.vendor.platformutils.is_android():
perm_ok = self.check_storage_permission()
if self.sideband.config["map_storage_external"]:
path = secondary_external_storage_path()
if path == None: path = primary_external_storage_path()
else:
path = primary_external_storage_path()
else:
perm_ok = True
if self.sideband.config["map_storage_external"]:
path = "/"
else:
path = os.path.expanduser("~")
else:
perm_ok = True
path = self.sideband.config["map_storage_path"]
if perm_ok and path != None:
try:
self.file_manager = MDFileManager(
exit_manager=self.map_fm_exited,
select_path=self.map_fm_got_path,
)
self.file_manager.ext = [".mbtiles"]
self.file_manager.show(path)
except Exception as e:
self.sideband.config["map_storage_path"] = None
self.sideband.save_configuration()
toast("Error reading directory, check permissions!")
else:
self.sideband.config["map_storage_path"] = None
self.sideband.save_configuration()
toast("No file access, check permissions!")
def map_get_offline_source(self): def map_get_offline_source(self):
if self.offline_source != None: if self.offline_source != None:
return self.offline_source return self.offline_source
@ -3819,47 +4073,6 @@ class SidebandApp(MDApp):
except Exception as e: except Exception as e:
RNS.log("Error while switching map layer: "+str(e), RNS.LOG_ERROR) RNS.log("Error while switching map layer: "+str(e), RNS.LOG_ERROR)
def map_select_file_action(self, sender=None):
perm_ok = False
if self.sideband.config["map_storage_path"] == None:
if RNS.vendor.platformutils.is_android():
perm_ok = self.check_storage_permission()
if self.sideband.config["map_storage_external"]:
path = secondary_external_storage_path()
if path == None: path = primary_external_storage_path()
else:
path = primary_external_storage_path()
else:
perm_ok = True
if self.sideband.config["map_storage_external"]:
path = "/"
else:
path = os.path.expanduser("~")
else:
perm_ok = True
path = self.sideband.config["map_storage_path"]
if perm_ok and path != None:
try:
self.file_manager = MDFileManager(
exit_manager=self.map_fm_exited,
select_path=self.map_fm_got_path,
)
self.file_manager.ext = [".mbtiles"]
self.file_manager.show(path)
except Exception as e:
self.sideband.config["map_storage_path"] = None
self.sideband.save_configuration()
toast("Error reading directory, check permissions!")
else:
self.sideband.config["map_storage_path"] = None
self.sideband.save_configuration()
toast("No file access, check permissions!")
map_nav_divisor = 12 map_nav_divisor = 12
map_nav_zoom = 0.25 map_nav_zoom = 0.25
def map_nav_left(self, sender=None, modifier=1.0): def map_nav_left(self, sender=None, modifier=1.0):
@ -4519,6 +4732,9 @@ Thank you very much for using Free Communications Systems.
class CustomOneLineIconListItem(OneLineIconListItem): class CustomOneLineIconListItem(OneLineIconListItem):
icon = StringProperty() icon = StringProperty()
class DialogItem(OneLineIconListItem):
divider = None
class MDMapIconButton(MDIconButton): class MDMapIconButton(MDIconButton):
pass pass

View File

@ -3244,7 +3244,7 @@ class SidebandCore():
RNS.log("Error while creating paper message: "+str(e), RNS.LOG_ERROR) RNS.log("Error while creating paper message: "+str(e), RNS.LOG_ERROR)
return False return False
def send_message(self, content, destination_hash, propagation, skip_fields=False, no_display=False): def send_message(self, content, destination_hash, propagation, skip_fields=False, no_display=False, attachment = None, image = None, audio = None):
try: try:
if content == "": if content == "":
raise ValueError("Message content cannot be empty") raise ValueError("Message content cannot be empty")
@ -3263,6 +3263,11 @@ class SidebandCore():
else: else:
fields = self.get_message_fields(destination_hash) fields = self.get_message_fields(destination_hash)
if attachment != None:
fields[LXMF.FIELD_FILE_ATTACHMENTS] = [attachment]
if image != None:
fields[LXMF.FIELD_IMAGE] = image
lxm = LXMF.LXMessage(dest, source, content, title="", desired_method=desired_method, fields = fields) lxm = LXMF.LXMessage(dest, source, content, title="", desired_method=desired_method, fields = fields)
if not no_display: if not no_display: