Add 'gtk/' from commit '7d0dfb455aa86245231b383a92e79b3c08a12d5e'
git-subtree-dir: gtk git-subtree-mainline:c710c6e053git-subtree-split:7d0dfb455a
This commit is contained in:
336
gtk/src/transcript/transcript-container-view.vala
Normal file
336
gtk/src/transcript/transcript-container-view.vala
Normal file
@@ -0,0 +1,336 @@
|
||||
using Gtk;
|
||||
using Adw;
|
||||
using Gee;
|
||||
using Gdk;
|
||||
using GLib;
|
||||
|
||||
class TranscriptContainerView : Adw.Bin
|
||||
{
|
||||
public TranscriptView transcript_view;
|
||||
|
||||
private Box container;
|
||||
private Button send_button;
|
||||
private FlowBox attachment_flow_box;
|
||||
|
||||
private TextView message_view;
|
||||
private TextBuffer message_buffer;
|
||||
private HashSet<string> pending_uploads;
|
||||
private HashMap<string, AttachmentPreview> attachment_previews;
|
||||
private ArrayList<UploadedAttachment> completed_attachments;
|
||||
|
||||
public string message_body {
|
||||
owned get {
|
||||
TextIter start_iter, end_iter;
|
||||
message_buffer.get_bounds(out start_iter, out end_iter);
|
||||
return message_buffer.get_text(start_iter, end_iter, false);
|
||||
}
|
||||
}
|
||||
|
||||
public ArrayList<string> attachment_guids {
|
||||
owned get {
|
||||
var attachment_guids = new ArrayList<string>();
|
||||
completed_attachments.foreach((attachment) => {
|
||||
attachment_guids.add(attachment.attachment_guid);
|
||||
return true;
|
||||
});
|
||||
|
||||
return attachment_guids;
|
||||
}
|
||||
}
|
||||
|
||||
private bool can_send {
|
||||
get {
|
||||
return (message_body.length > 0 || completed_attachments.size > 0) && pending_uploads.size == 0;
|
||||
}
|
||||
}
|
||||
|
||||
public TranscriptContainerView () {
|
||||
container = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
|
||||
set_child (container);
|
||||
|
||||
pending_uploads = new HashSet<string>();
|
||||
attachment_previews = new HashMap<string, AttachmentPreview>();
|
||||
completed_attachments = new ArrayList<UploadedAttachment>();
|
||||
|
||||
// Create message list view
|
||||
transcript_view = new TranscriptView();
|
||||
transcript_view.set_vexpand(true);
|
||||
container.append(transcript_view);
|
||||
|
||||
// Create attachment preview row (initially hidden)
|
||||
setup_attachment_row();
|
||||
|
||||
// Create bottom box for input
|
||||
var input_box = new Box(Orientation.HORIZONTAL, 6);
|
||||
input_box.add_css_class("message-input-box");
|
||||
input_box.set_valign(Align.END);
|
||||
input_box.set_spacing(6);
|
||||
container.append(input_box);
|
||||
|
||||
// Setup drag and drop
|
||||
setup_drag_and_drop();
|
||||
|
||||
// Connect to repository signals
|
||||
Repository.get_instance().attachment_uploaded.connect(on_attachment_uploaded);
|
||||
|
||||
// Create attach button (paperclip)
|
||||
var attach_button = new Button.from_icon_name("mail-attachment");
|
||||
attach_button.set_tooltip_text("Attach file…");
|
||||
attach_button.add_css_class("flat");
|
||||
attach_button.clicked.connect(on_attach_button_clicked);
|
||||
input_box.append(attach_button);
|
||||
|
||||
// Create message text view (added after attachment button so button stays to the left)
|
||||
message_buffer = new TextBuffer(null);
|
||||
message_view = new TextView.with_buffer(message_buffer);
|
||||
message_view.add_css_class("message-input-entry");
|
||||
message_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR);
|
||||
message_view.set_hexpand(true);
|
||||
message_view.set_vexpand(false);
|
||||
message_view.set_size_request(-1, 12); // intrinsic
|
||||
message_buffer.changed.connect(on_text_changed);
|
||||
|
||||
// Key controller for sending on Enter (Shift+Enter for newline)
|
||||
var send_key_ctrl = new EventControllerKey();
|
||||
send_key_ctrl.key_pressed.connect((keyval, keycode, state) => {
|
||||
if (keyval == Gdk.Key.Return && (state & Gdk.ModifierType.SHIFT_MASK) == 0) {
|
||||
on_request_send();
|
||||
return true; // consume
|
||||
}
|
||||
return false;
|
||||
});
|
||||
message_view.add_controller(send_key_ctrl);
|
||||
|
||||
// Handle paste events to detect images
|
||||
message_view.paste_clipboard.connect(on_message_paste_clipboard);
|
||||
|
||||
input_box.append(message_view);
|
||||
|
||||
// Create send button
|
||||
send_button = new Button();
|
||||
send_button.set_label("Send");
|
||||
send_button.set_sensitive(false);
|
||||
send_button.add_css_class("suggested-action");
|
||||
send_button.clicked.connect(on_request_send);
|
||||
input_box.append(send_button);
|
||||
}
|
||||
|
||||
private void setup_attachment_row() {
|
||||
attachment_flow_box = new FlowBox();
|
||||
attachment_flow_box.set_max_children_per_line(6);
|
||||
attachment_flow_box.set_row_spacing(6);
|
||||
attachment_flow_box.set_column_spacing(6);
|
||||
attachment_flow_box.halign = Align.START;
|
||||
attachment_flow_box.add_css_class("attachment-preview-row");
|
||||
container.append(attachment_flow_box);
|
||||
}
|
||||
|
||||
private void setup_drag_and_drop() {
|
||||
var drop_target = new DropTarget(typeof(File), Gdk.DragAction.COPY);
|
||||
drop_target.drop.connect(on_file_dropped);
|
||||
this.add_controller(drop_target);
|
||||
}
|
||||
|
||||
private bool on_file_dropped(Value val, double x, double y) {
|
||||
if (!val.holds(typeof(File))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var file = (File)val.get_object();
|
||||
if (file == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if it's an image file
|
||||
try {
|
||||
var file_info = file.query_info(FileAttribute.STANDARD_CONTENT_TYPE, FileQueryInfoFlags.NONE);
|
||||
string content_type = file_info.get_content_type();
|
||||
|
||||
if (!content_type.has_prefix("image/")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
upload_file(file);
|
||||
return true;
|
||||
} catch (Error e) {
|
||||
warning("Failed to get file info: %s", e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void upload_file(File file) {
|
||||
try {
|
||||
string upload_guid = Repository.get_instance().upload_attachment(file.get_path());
|
||||
pending_uploads.add(upload_guid);
|
||||
|
||||
var preview = new AttachmentPreview(file, upload_guid);
|
||||
preview.remove_requested.connect(() => {
|
||||
remove_attachment(upload_guid);
|
||||
});
|
||||
|
||||
attachment_previews[upload_guid] = preview;
|
||||
attachment_flow_box.append(preview);
|
||||
|
||||
update_attachment_row_visibility();
|
||||
} catch (Error e) {
|
||||
warning("Failed to upload attachment: %s", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_attachment_uploaded(string upload_guid, string attachment_guid) {
|
||||
if (attachment_previews.has_key(upload_guid)) {
|
||||
var preview = attachment_previews[upload_guid];
|
||||
preview.set_completed(attachment_guid);
|
||||
completed_attachments.add(new UploadedAttachment(upload_guid, attachment_guid));
|
||||
pending_uploads.remove(upload_guid);
|
||||
update_send_button_sensitivity();
|
||||
}
|
||||
}
|
||||
|
||||
private void remove_attachment(string upload_guid) {
|
||||
if (attachment_previews.has_key(upload_guid)) {
|
||||
var preview = attachment_previews[upload_guid];
|
||||
attachment_flow_box.remove(preview);
|
||||
attachment_previews.unset(upload_guid);
|
||||
|
||||
completed_attachments.foreach((attachment) => {
|
||||
if (attachment.upload_guid == upload_guid) {
|
||||
completed_attachments.remove(attachment);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
pending_uploads.remove(upload_guid);
|
||||
|
||||
update_attachment_row_visibility();
|
||||
update_send_button_sensitivity();
|
||||
}
|
||||
}
|
||||
|
||||
private void update_attachment_row_visibility() {
|
||||
bool has_attachments = attachment_previews.size > 0;
|
||||
attachment_flow_box.set_visible(has_attachments);
|
||||
}
|
||||
|
||||
private void on_text_changed() {
|
||||
update_send_button_sensitivity();
|
||||
}
|
||||
|
||||
private void update_send_button_sensitivity() {
|
||||
send_button.set_sensitive(can_send);
|
||||
}
|
||||
|
||||
private void on_request_send() {
|
||||
if (can_send) {
|
||||
on_send();
|
||||
|
||||
// Clear the message text
|
||||
message_buffer.set_text("");
|
||||
|
||||
// Clear the attachment previews
|
||||
attachment_flow_box.remove_all();
|
||||
attachment_previews.clear();
|
||||
completed_attachments.clear();
|
||||
pending_uploads.clear();
|
||||
|
||||
update_send_button_sensitivity();
|
||||
}
|
||||
}
|
||||
|
||||
private void on_send() {
|
||||
var body = message_body;
|
||||
|
||||
// Strip empty space at the beginning and end of the body
|
||||
body = body.strip();
|
||||
|
||||
if (transcript_view.model == null) {
|
||||
GLib.warning("No conversation selected");
|
||||
return;
|
||||
}
|
||||
|
||||
var selected_conversation = transcript_view.model.conversation;
|
||||
if (selected_conversation == null) {
|
||||
GLib.warning("No conversation selected");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Repository.get_instance().send_message(selected_conversation.guid, body, attachment_guids.to_array());
|
||||
} catch (Error e) {
|
||||
GLib.warning("Failed to send message: %s", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_attach_button_clicked() {
|
||||
var dialog = new Gtk.FileDialog();
|
||||
dialog.set_title("Select attachment");
|
||||
dialog.set_accept_label("Attach");
|
||||
dialog.set_modal(true);
|
||||
|
||||
// Images only for now
|
||||
var filter = new Gtk.FileFilter();
|
||||
filter.set_filter_name("Images");
|
||||
filter.add_mime_type("image/png");
|
||||
filter.add_mime_type("image/jpeg");
|
||||
filter.add_mime_type("image/gif");
|
||||
filter.add_mime_type("image/bmp");
|
||||
filter.add_mime_type("image/webp");
|
||||
filter.add_mime_type("image/svg+xml");
|
||||
filter.add_mime_type("image/tiff");
|
||||
|
||||
dialog.set_default_filter(filter);
|
||||
|
||||
var parent_window = get_root() as Gtk.Window;
|
||||
dialog.open.begin(parent_window, null, (obj, res) => {
|
||||
try {
|
||||
var file = dialog.open.end(res);
|
||||
if (file != null) {
|
||||
upload_file(file);
|
||||
}
|
||||
} catch (Error e) {
|
||||
warning("Failed to open file dialog: %s", e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void on_message_paste_clipboard() {
|
||||
var display = get_display();
|
||||
if (display == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var clipboard = display.get_clipboard();
|
||||
if (clipboard == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
clipboard.read_texture_async.begin(null, (obj, res) => {
|
||||
try {
|
||||
var clip = obj as Gdk.Clipboard;
|
||||
var texture = clip.read_texture_async.end(res);
|
||||
if (texture != null) {
|
||||
string tmp_path = Path.build_filename(Environment.get_tmp_dir(), "clipboard-" + Uuid.string_random() + ".png");
|
||||
texture.save_to_png(tmp_path);
|
||||
var tmp_file = File.new_for_path(tmp_path);
|
||||
upload_file(tmp_file);
|
||||
}
|
||||
} catch (Error e) {
|
||||
// Ignore if clipboard does not contain image
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class UploadedAttachment
|
||||
{
|
||||
public string upload_guid;
|
||||
public string attachment_guid;
|
||||
|
||||
public UploadedAttachment(string upload_guid, string attachment_guid)
|
||||
{
|
||||
this.upload_guid = upload_guid;
|
||||
this.attachment_guid = attachment_guid;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user