diff --git a/db_handler.py b/db_handler.py index 171d0cc..a9aab2d 100644 --- a/db_handler.py +++ b/db_handler.py @@ -101,7 +101,7 @@ def load_messages_from_db(): channel = int(channel) if channel.isdigit() else channel # Add the channel to globals.channel_list if not already present - if channel not in globals.channel_list: + if channel not in globals.channel_list and not is_chat_archived(channel): globals.channel_list.append(channel) # Ensure the channel exists in globals.all_messages @@ -189,7 +189,7 @@ def maybe_store_nodeinfo_in_db(packet): logging.error(f"Unexpected error in maybe_store_nodeinfo_in_db: {e}") -def update_node_info_in_db(user_id, long_name=None, short_name=None, hw_model=None, is_licensed=None, role=None, public_key=None): +def update_node_info_in_db(user_id, long_name=None, short_name=None, hw_model=None, is_licensed=None, role=None, public_key=None, chat_archived=None): """Update or insert node information into the database, preserving unchanged fields.""" try: ensure_node_table_exists() # Ensure the table exists before any operation @@ -198,12 +198,18 @@ def update_node_info_in_db(user_id, long_name=None, short_name=None, hw_model=No db_cursor = db_connection.cursor() table_name = f'"{globals.myNodeNum}_nodedb"' # Quote in case of numeric names + + table_columns = [i[1] for i in db_cursor.execute(f'PRAGMA table_info({table_name})')] + if "chat_archived" not in table_columns: + update_table_query = f"ALTER TABLE {table_name} ADD COLUMN chat_archived INTEGER" + db_cursor.execute(update_table_query) + # Fetch existing values to preserve unchanged fields db_cursor.execute(f'SELECT * FROM {table_name} WHERE user_id = ?', (user_id,)) existing_record = db_cursor.fetchone() if existing_record: - existing_long_name, existing_short_name, existing_hw_model, existing_is_licensed, existing_role, existing_public_key = existing_record[1:] + existing_long_name, existing_short_name, existing_hw_model, existing_is_licensed, existing_role, existing_public_key, existing_chat_archived = existing_record[1:] long_name = long_name if long_name is not None else existing_long_name short_name = short_name if short_name is not None else existing_short_name @@ -211,20 +217,22 @@ def update_node_info_in_db(user_id, long_name=None, short_name=None, hw_model=No is_licensed = is_licensed if is_licensed is not None else existing_is_licensed role = role if role is not None else existing_role public_key = public_key if public_key is not None else existing_public_key + chat_archived = chat_archived if chat_archived is not None else existing_chat_archived # Upsert logic upsert_query = f''' - INSERT INTO {table_name} (user_id, long_name, short_name, hw_model, is_licensed, role, public_key) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT INTO {table_name} (user_id, long_name, short_name, hw_model, is_licensed, role, public_key, chat_archived) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET long_name = excluded.long_name, short_name = excluded.short_name, hw_model = excluded.hw_model, is_licensed = excluded.is_licensed, role = excluded.role, - public_key = excluded.public_key + public_key = excluded.public_key, + chat_archived = excluded.chat_archived ''' - db_cursor.execute(upsert_query, (user_id, long_name, short_name, hw_model, is_licensed, role, public_key)) + db_cursor.execute(upsert_query, (user_id, long_name, short_name, hw_model, is_licensed, role, public_key, chat_archived)) db_connection.commit() except sqlite3.Error as e: @@ -243,7 +251,8 @@ def ensure_node_table_exists(): hw_model TEXT, is_licensed TEXT, role TEXT, - public_key TEXT + public_key TEXT, + chat_archived INTEGER ''' ensure_table_exists(table_name, schema) @@ -294,4 +303,25 @@ def get_name_from_database(user_id, type="long"): except Exception as e: logging.error(f"Unexpected error in get_name_from_database: {e}") - return "Unknown" \ No newline at end of file + return "Unknown" + +def is_chat_archived(user_id): + try: + with sqlite3.connect(config.db_file_path) as db_connection: + db_cursor = db_connection.cursor() + table_name = f"{str(globals.myNodeNum)}_nodedb" + nodeinfo_table = f'"{table_name}"' + query = f"SELECT chat_archived FROM {nodeinfo_table} WHERE user_id = ?" + db_cursor.execute(query, (user_id,)) + result = db_cursor.fetchone() + + return result[0] if result else 0 + + except sqlite3.Error as e: + logging.error(f"SQLite error in is_chat_archived: {e}") + return "Unknown" + + except Exception as e: + logging.error(f"Unexpected error in is_chat_archived: {e}") + return "Unknown" + diff --git a/globals.py b/globals.py index 72ab0ad..a174cf8 100644 --- a/globals.py +++ b/globals.py @@ -2,7 +2,7 @@ interface = None display_log = False all_messages = {} channel_list = [] -notifications = set() +notifications = [] packet_buffer = [] node_list = [] myNodeNum = 0 diff --git a/message_handlers/rx_handler.py b/message_handlers/rx_handler.py index d8dc31e..0261e3d 100644 --- a/message_handlers/rx_handler.py +++ b/message_handlers/rx_handler.py @@ -3,7 +3,7 @@ import time from utilities.utils import refresh_node_list from datetime import datetime from ui.curses_ui import draw_packetlog_win, draw_node_list, draw_messages_window, draw_channel_list, add_notification -from db_handler import save_message_to_db, maybe_store_nodeinfo_in_db, get_name_from_database +from db_handler import save_message_to_db, maybe_store_nodeinfo_in_db, get_name_from_database, update_node_info_in_db import default_config as config import globals @@ -50,7 +50,9 @@ def on_receive(packet, interface): pass else: globals.channel_list.append(packet['from']) - globals.all_messages[packet['from']] = [] + if(packet['from'] not in globals.all_messages): + globals.all_messages[packet['from']] = [] + update_node_info_in_db(packet['from'], chat_archived=False) refresh_channels = True channel_number = globals.channel_list.index(packet['from']) diff --git a/ui/curses_ui.py b/ui/curses_ui.py index 2034d21..dceac5e 100644 --- a/ui/curses_ui.py +++ b/ui/curses_ui.py @@ -4,7 +4,7 @@ from utilities.utils import get_channels, get_readable_duration, get_time_ago, r from settings import settings_menu from message_handlers.tx_handler import send_message, send_traceroute from ui.colors import setup_colors, get_color -from db_handler import get_name_from_database +from db_handler import get_name_from_database, update_node_info_in_db, is_chat_archived import default_config as config import ui.dialog import globals @@ -50,16 +50,16 @@ def draw_node_details(): ]) for s in node_details_list: - if len(nodestr) + len(s) < width: + if len(nodestr) + len(s) < width - 2: nodestr = nodestr + s draw_centered_text_field(function_win, nodestr, 0, get_color("commands")) def draw_function_win(): - cmds = ["↑→↓← = Select", " ENTER = Send", " ` = Settings", " ^P = Packet Log", " ESC = Quit"] + cmds = ["↑→↓← = Select", " ENTER = Send", " ` = Settings", " ^P = Packet Log", " ESC = Quit", " ^t = Traceroute", " ^d = Archive Chat"] function_str = "" for s in cmds: - if(len(function_str) + len(s) < function_win.getmaxyx()[1]): + if(len(function_str) + len(s) < function_win.getmaxyx()[1] - 2): function_str += s draw_centered_text_field(function_win, function_str, 0, get_color("commands")) @@ -113,7 +113,7 @@ def highlight_line(highlight, window, line): pad.chgat(line, 1, select_len, nd_color | curses.A_REVERSE if highlight else nd_color) if(window == 0): - channel = list(globals.all_messages.keys())[line] + channel = globals.channel_list[line] win_width = channel_box.getmaxyx()[1] if(isinstance(channel, int)): @@ -126,10 +126,12 @@ def highlight_line(highlight, window, line): pad.chgat(line, 1, select_len, ch_color | curses.A_REVERSE if highlight else ch_color) def add_notification(channel_number): - globals.notifications.add(channel_number) + if channel_number not in globals.notifications: + globals.notifications.append(channel_number) def remove_notification(channel_number): - globals.notifications.discard(channel_number) + if channel_number in globals.notifications: + globals.notifications.remove(channel_number) def draw_text_field(win, text, color): win.border() @@ -180,24 +182,28 @@ def draw_channel_list(): channel_pad.resize(len(globals.all_messages), channel_box.getmaxyx()[1]) - for i, channel in enumerate(list(globals.all_messages.keys())): + idx = 0 + for channel in globals.channel_list: # Convert node number to long name if it's an integer if isinstance(channel, int): + if is_chat_archived(channel): + continue channel = get_name_from_database(channel, type='long') # Determine whether to add the notification - notification = " " + config.notification_symbol if i in globals.notifications else "" + notification = " " + config.notification_symbol if idx in globals.notifications else "" # Truncate the channel name if it's too long to fit in the window truncated_channel = channel[:win_width - 5] + '-' if len(channel) > win_width - 5 else channel - if i == globals.selected_channel: + if idx == globals.selected_channel: if globals.current_window == 0: - channel_pad.addstr(i, 1, truncated_channel + notification, get_color("channel_list", reverse=True)) + channel_pad.addstr(idx, 1, truncated_channel + notification, get_color("channel_list", reverse=True)) remove_notification(globals.selected_channel) else: - channel_pad.addstr(i, 1, truncated_channel + notification, get_color("channel_selected")) + channel_pad.addstr(idx, 1, truncated_channel + notification, get_color("channel_selected")) else: - channel_pad.addstr(i, 1, truncated_channel + notification, get_color("channel_list")) + channel_pad.addstr(idx, 1, truncated_channel + notification, get_color("channel_list")) + idx += 1 channel_box.attrset(get_color("window_frame_selected") if globals.current_window == 0 else get_color("window_frame")) channel_box.box() @@ -477,10 +483,6 @@ def main_ui(stdscr): elif globals.current_window == 2: scroll_nodes(-1) - elif char == curses.KEY_RESIZE: - input_text = "" - handle_resize(stdscr, False) - elif char == curses.KEY_DOWN: if globals.current_window == 0: scroll_channels(1) @@ -591,9 +593,15 @@ def main_ui(stdscr): node_list = globals.node_list if node_list[globals.selected_node] not in globals.channel_list: globals.channel_list.append(node_list[globals.selected_node]) + if(node_list[globals.selected_node] not in globals.all_messages): globals.all_messages[node_list[globals.selected_node]] = [] + globals.selected_channel = globals.channel_list.index(node_list[globals.selected_node]) + + if(is_chat_archived(globals.channel_list[globals.selected_channel])): + update_node_info_in_db(globals.channel_list[globals.selected_channel], chat_archived=False) + globals.selected_node = 0 globals.current_window = 0 @@ -635,6 +643,28 @@ def main_ui(stdscr): globals.display_log = False packetlog_win.erase() draw_messages_window(True) + + elif char == curses.KEY_RESIZE: + input_text = "" + handle_resize(stdscr, False) + + # ^D + elif char == chr(4): + if(globals.current_window == 0): + if(isinstance(globals.channel_list[globals.selected_channel], int)): + update_node_info_in_db(globals.channel_list[globals.selected_channel], chat_archived=True) + + # Shift notifications up to account for deleted item + for i in range(len(globals.notifications)): + if globals.notifications[i] > globals.selected_channel: + globals.notifications[i] -= 1 + + del globals.channel_list[globals.selected_channel] + globals.selected_channel = min(globals.selected_channel, len(globals.channel_list) - 1) + select_channel(globals.selected_channel) + draw_channel_list() + draw_messages_window() + else: # Append typed character to input text if(isinstance(char, str)):