messages.c (10400B)
1 /* 2 This file is part of GNUnet. 3 Copyright (C) 2022--2025 GNUnet e.V. 4 5 GNUnet is free software: you can redistribute it and/or modify it 6 under the terms of the GNU Affero General Public License as published 7 by the Free Software Foundation, either version 3 of the License, 8 or (at your option) any later version. 9 10 GNUnet is distributed in the hope that it will be useful, but 11 WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 Affero General Public License for more details. 14 15 You should have received a copy of the GNU Affero General Public License 16 along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 SPDX-License-Identifier: AGPL3.0-or-later 19 */ 20 /* 21 * @author Tobias Frisch 22 * @file ui/messages.c 23 */ 24 25 #include "messages.h" 26 27 #include "list_input.h" 28 #include "text_input.h" 29 #include "../application.h" 30 #include "../util.h" 31 #include <gnunet/gnunet_chat_lib.h> 32 #include <gnunet/gnunet_common.h> 33 34 struct tm* 35 _messages_new_day(time_t* current_time, 36 const time_t* timestamp) 37 { 38 struct tm* ts = localtime(timestamp); 39 40 ts->tm_sec = 0; 41 ts->tm_min = 0; 42 ts->tm_hour = 0; 43 44 const time_t date_time = timelocal(ts); 45 46 if (date_time <= *current_time) { 47 return NULL; 48 } 49 50 *current_time = date_time; 51 return ts; 52 } 53 54 void 55 _messages_handle_message(UI_MESSAGES_Handle *messages) 56 { 57 switch (GNUNET_CHAT_message_get_kind(messages->selected)) 58 { 59 case GNUNET_CHAT_KIND_INVITATION: 60 { 61 struct GNUNET_CHAT_Invitation *invitation = ( 62 GNUNET_CHAT_message_get_invitation(messages->selected) 63 ); 64 65 if (invitation) 66 GNUNET_CHAT_invitation_accept(invitation); 67 break; 68 } 69 case GNUNET_CHAT_KIND_FILE: 70 { 71 struct GNUNET_CHAT_File *file = GNUNET_CHAT_message_get_file( 72 messages->selected 73 ); 74 75 if ((file) && (GNUNET_YES != GNUNET_CHAT_file_is_downloading(file))) 76 GNUNET_CHAT_file_start_download(file, NULL, NULL); 77 break; 78 default: 79 break; 80 } 81 } 82 } 83 84 void 85 messages_event(UI_MESSAGES_Handle *messages, 86 MESSENGER_Application *app, 87 int key) 88 { 89 list_input_reset(messages); 90 messages->line_time = 0; 91 92 UI_MESSAGES_List *element = messages->head; 93 while (element) 94 { 95 struct tm *ts = _messages_new_day( 96 &(messages->line_time), 97 &(element->timestamp) 98 ); 99 100 list_input_select(messages, ts? 2 : 1, element->message); 101 element = element->next; 102 } 103 104 list_input_select(messages, 1, NULL); 105 106 switch (key) 107 { 108 case 27: 109 case KEY_EXIT: 110 app->chat.context = NULL; 111 break; 112 case '\t': 113 app->chat.show_members = TRUE; 114 break; 115 case '\n': 116 case KEY_ENTER: 117 if (messages->selected) 118 _messages_handle_message(messages); 119 else if (messages->text_len > 0) 120 { 121 if (0 != strncmp(messages->text, 122 UI_MESSAGES_FILE_PREFIX, 123 UI_MESSAGES_FILE_PREFIX_LEN)) 124 goto write_text; 125 126 const char* filename = messages->text + 5; 127 128 if (0 != access(filename, R_OK | F_OK)) 129 break; 130 131 GNUNET_CHAT_context_send_file( 132 app->chat.context, 133 filename, 134 NULL, 135 NULL 136 ); 137 138 goto drop_text; 139 140 write_text: 141 GNUNET_CHAT_context_send_text( 142 app->chat.context, 143 messages->text 144 ); 145 146 drop_text: 147 messages->text_len = 0; 148 } 149 break; 150 case KEY_BACKSPACE: 151 if (messages->selected) 152 GNUNET_CHAT_message_delete( 153 messages->selected, 0 154 ); 155 break; 156 default: 157 break; 158 } 159 160 if (!(messages->selected)) 161 text_input_event(messages->text, key); 162 163 list_input_event(messages, key); 164 } 165 166 void 167 _messages_iterate_print(UI_MESSAGES_Handle *messages, 168 const time_t* timestamp, 169 struct GNUNET_CHAT_Message *message) 170 { 171 static const char *you = "you"; 172 173 enum GNUNET_CHAT_MessageKind kind = GNUNET_CHAT_message_get_kind(message); 174 175 struct GNUNET_CHAT_Contact *sender = GNUNET_CHAT_message_get_sender(message); 176 struct GNUNET_CHAT_Contact *recipient = GNUNET_CHAT_message_get_recipient(message); 177 178 enum GNUNET_GenericReturnValue sent = GNUNET_CHAT_message_is_sent(message); 179 const char *msg_s = GNUNET_YES == sent? "" : "s"; 180 181 enum GNUNET_GenericReturnValue recv = recipient? 182 GNUNET_CHAT_contact_is_owned(recipient) : GNUNET_NO; 183 184 const char *name = GNUNET_YES == sent? you : ( 185 sender? GNUNET_CHAT_contact_get_name(sender) : NULL 186 ); 187 188 const char *rcp = GNUNET_YES == recv? you : ( 189 recipient? GNUNET_CHAT_contact_get_name(recipient) : you 190 ); 191 192 const char *text = GNUNET_CHAT_message_get_text(message); 193 194 const struct GNUNET_CHAT_File *file = GNUNET_CHAT_message_get_file(message); 195 196 struct tm* ts = localtime(timestamp); 197 char time_buf [255]; 198 199 strftime(time_buf, sizeof(time_buf), "%H:%M", ts); 200 201 ts = _messages_new_day(&(messages->line_time), timestamp); 202 203 list_input_print(messages, ts? 2 : 1); 204 wmove(messages->window, y, 0); 205 206 if (ts) { 207 char date_buf [255]; 208 209 strftime(date_buf, sizeof(date_buf), "%x", ts); 210 211 const int width = getmaxx(messages->window); 212 213 whline(messages->window, '-', width); 214 wmove(messages->window, y, 8); 215 216 wprintw(messages->window, " %s ", date_buf); 217 wmove(messages->window, y+1, 0); 218 } 219 220 const int attrs_select = A_BOLD; 221 222 if (selected) wattron(messages->window, attrs_select); 223 224 wprintw(messages->window, " %s | ", time_buf); 225 226 util_enable_unique_color(messages->window, sender); 227 228 switch (kind) { 229 case GNUNET_CHAT_KIND_JOIN: 230 wprintw( 231 messages->window, 232 "%s join%s the room.", 233 name, 234 msg_s 235 ); 236 break; 237 case GNUNET_CHAT_KIND_LEAVE: 238 wprintw( 239 messages->window, 240 "%s leave%s the room.", 241 name, 242 msg_s 243 ); 244 break; 245 case GNUNET_CHAT_KIND_INVITATION: 246 wprintw( 247 messages->window, 248 "%s invite%s %s to a room.", 249 name, 250 msg_s, 251 rcp 252 ); 253 break; 254 case GNUNET_CHAT_KIND_TEXT: 255 wprintw( 256 messages->window, 257 "%s: %s", 258 name, 259 text 260 ); 261 break; 262 case GNUNET_CHAT_KIND_FILE: { 263 const char *filename = GNUNET_CHAT_file_get_name(file); 264 265 const uint64_t localsize = GNUNET_CHAT_file_get_local_size(file); 266 const uint64_t filesize = GNUNET_CHAT_file_get_size(file); 267 268 wprintw( 269 messages->window, 270 "%s share%s the file '%s' (%lu / %lu).", 271 name, 272 msg_s, 273 filename, 274 localsize, 275 filesize 276 ); 277 break; 278 } 279 default: 280 wprintw( 281 messages->window, 282 "[%d] %s: %s", 283 (int) kind, 284 name, 285 text 286 ); 287 break; 288 } 289 290 util_disable_unique_color(messages->window, sender); 291 292 if (selected) wattroff(messages->window, attrs_select); 293 } 294 295 void 296 messages_print(UI_MESSAGES_Handle *messages) 297 { 298 if (!(messages->window)) 299 return; 300 301 list_input_reset(messages); 302 messages->line_time = 0; 303 304 werase(messages->window); 305 306 UI_MESSAGES_List *element = messages->head; 307 while (element) 308 { 309 _messages_iterate_print(messages, &(element->timestamp), element->message); 310 element = element->next; 311 } 312 313 const int count = messages->line_index; 314 const bool selected = (count == messages->line_selected); 315 316 const int width = getmaxx(messages->window); 317 const int height = getmaxy(messages->window); 318 const int line_height = height - 2; 319 320 wmove(messages->window, line_height, 0); 321 whline(messages->window, '-', width); 322 323 const bool is_file_text = (0 == strncmp( 324 messages->text, 325 UI_MESSAGES_FILE_PREFIX, 326 UI_MESSAGES_FILE_PREFIX_LEN 327 )); 328 329 const int attrs_select = A_BOLD | (is_file_text? A_ITALIC : A_NORMAL); 330 331 if (selected) wattron(messages->window, attrs_select); 332 333 wmove(messages->window, height - 1, 0); 334 wprintw(messages->window, "%s", messages->text); 335 336 if (selected) wattroff(messages->window, attrs_select); 337 338 wmove(messages->window, height - 1, messages->text_pos); 339 340 if (selected) 341 { 342 wcursyncup(messages->window); 343 curs_set(1); 344 } 345 } 346 347 void 348 messages_clear(UI_MESSAGES_Handle *messages) 349 { 350 UI_MESSAGES_List *element; 351 while (messages->head) 352 { 353 element = messages->head; 354 355 GNUNET_CONTAINER_DLL_remove( 356 messages->head, 357 messages->tail, 358 element 359 ); 360 361 GNUNET_free(element); 362 } 363 } 364 365 static int 366 _message_compare_timestamps(UNUSED void *cls, 367 UI_MESSAGES_List *list0, 368 UI_MESSAGES_List *list1) 369 { 370 if ((!list0) || (!list1)) 371 return 0; 372 373 if (list0->timestamp > list1->timestamp) 374 return -1; 375 else if (list0->timestamp < list1->timestamp) 376 return +1; 377 else 378 return 0; 379 } 380 381 void 382 messages_add(UI_MESSAGES_Handle *messages, 383 struct GNUNET_CHAT_Message *message) 384 { 385 enum GNUNET_CHAT_MessageKind kind = GNUNET_CHAT_message_get_kind(message); 386 387 switch (kind) { 388 case GNUNET_CHAT_KIND_UPDATE_CONTEXT: 389 case GNUNET_CHAT_KIND_CONTACT: 390 case GNUNET_CHAT_KIND_DELETION: 391 return; 392 default: 393 break; 394 } 395 396 list_input_reset(messages); 397 messages->line_time = 0; 398 399 UI_MESSAGES_List *element = messages->head; 400 while (element) 401 { 402 struct tm *ts = _messages_new_day( 403 &(messages->line_time), 404 &(element->timestamp) 405 ); 406 407 list_input_select(messages, ts? 2 : 1, element->message); 408 element = element->next; 409 } 410 411 list_input_select(messages, 1, NULL); 412 413 const time_t timestamp = ( 414 GNUNET_CHAT_message_get_timestamp(message) 415 ); 416 417 element = GNUNET_new(UI_MESSAGES_List); 418 element->timestamp = timestamp; 419 element->message = message; 420 421 GNUNET_CONTAINER_DLL_insert_sorted( 422 UI_MESSAGES_List, 423 _message_compare_timestamps, 424 NULL, 425 messages->head, 426 messages->tail, 427 element 428 ); 429 430 list_input_select(messages, 1, NULL); 431 432 if (!(messages->selected)) 433 list_input_event(messages, KEY_DOWN); 434 } 435 436 void 437 messages_remove(UI_MESSAGES_Handle *messages, 438 struct GNUNET_CHAT_Message *message) 439 { 440 UI_MESSAGES_List *element = messages->head; 441 while (element) 442 { 443 if (element->message == message) 444 break; 445 446 element = element->next; 447 } 448 449 if (element) 450 GNUNET_CONTAINER_DLL_remove( 451 messages->head, 452 messages->tail, 453 element 454 ); 455 }