messenger-cli

Command-line user interface for GNUnet Messenger
Log | Files | Refs | README | LICENSE

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 }