ConversationView.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. /*
  2. * Copyright 2009-2011, Andrea Anzani. All rights reserved.
  3. * Copyright 2021, Jaidyn Levesque. All rights reserved.
  4. * Distributed under the terms of the MIT License.
  5. *
  6. * Authors:
  7. * Andrea Anzani, andrea.anzani@gmail.com
  8. * Jaidyn Levesque, jadedctrl@teknik.io
  9. */
  10. #include "ConversationView.h"
  11. #include <Catalog.h>
  12. #include <LayoutBuilder.h>
  13. #include <ListView.h>
  14. #include <ScrollView.h>
  15. #include <SplitView.h>
  16. #include <StringList.h>
  17. #include <StringView.h>
  18. #include <libinterface/BitmapView.h>
  19. #include <libinterface/EnterTextView.h>
  20. #include "AppMessages.h"
  21. #include "AppPreferences.h"
  22. #include "ChatOMatic.h"
  23. #include "ChatProtocolMessages.h"
  24. #include "Conversation.h"
  25. #include "NotifyMessage.h"
  26. #include "ProtocolManager.h"
  27. #include "RenderView.h"
  28. #include "SendTextView.h"
  29. #include "User.h"
  30. #include "UserListView.h"
  31. #include "Utils.h"
  32. #undef B_TRANSLATION_CONTEXT
  33. #define B_TRANSLATION_CONTEXT "ConversationView"
  34. ConversationView::ConversationView(Conversation* chat)
  35. :
  36. BGroupView("chatView", B_VERTICAL, B_USE_DEFAULT_SPACING),
  37. fMessageQueue(),
  38. fConversation(chat)
  39. {
  40. _InitInterface();
  41. if (chat != NULL) {
  42. SetConversation(chat);
  43. fUserList->SetConversation(chat);
  44. }
  45. else
  46. _FakeChat();
  47. }
  48. void
  49. ConversationView::AttachedToWindow()
  50. {
  51. while (fMessageQueue.IsEmpty() == false) {
  52. BMessage* msg = fMessageQueue.RemoveItemAt(0);
  53. MessageReceived(msg);
  54. }
  55. if (fConversation != NULL) {
  56. if (fNameTextView->Text() != fConversation->GetName())
  57. fNameTextView->SetText(fConversation->GetName());
  58. if (fSubjectTextView->Text() != fConversation->GetSubject())
  59. fSubjectTextView->SetText(fConversation->GetSubject());
  60. }
  61. NotifyInteger(INT_WINDOW_FOCUSED, 0);
  62. fSendView->MakeFocus(true);
  63. fSendView->Invalidate();
  64. }
  65. void
  66. ConversationView::MessageReceived(BMessage* message)
  67. {
  68. switch (message->what) {
  69. case APP_CHAT:
  70. {
  71. BString text = fSendView->Text();
  72. if (fConversation == NULL || text == "")
  73. return;
  74. int64 instance = fConversation->GetProtocolLooper()->GetInstance();
  75. BMessage msg(IM_MESSAGE);
  76. msg.AddInt32("im_what", IM_SEND_MESSAGE);
  77. msg.AddInt64("instance", instance);
  78. msg.AddString("chat_id", fConversation->GetId());
  79. msg.AddString("body", text);
  80. fConversation->ImMessage(&msg);
  81. fSendView->SetText("");
  82. fSendView->ScrollToOffset(0);
  83. break;
  84. }
  85. case kClearText:
  86. _AppendOrEnqueueMessage(message);
  87. break;
  88. case IM_MESSAGE:
  89. ImMessage(message);
  90. break;
  91. default:
  92. BGroupView::MessageReceived(message);
  93. break;
  94. }
  95. }
  96. void
  97. ConversationView::ImMessage(BMessage* msg)
  98. {
  99. int32 im_what = msg->FindInt32("im_what");
  100. switch (im_what) {
  101. case IM_ROOM_LEFT:
  102. {
  103. delete fConversation;
  104. delete this;
  105. break;
  106. }
  107. case IM_MESSAGE_RECEIVED:
  108. {
  109. _AppendOrEnqueueMessage(msg);
  110. fReceiveView->ScrollToBottom();
  111. break;
  112. }
  113. case IM_MESSAGE_SENT:
  114. case IM_LOGS_RECEIVED:
  115. {
  116. _AppendOrEnqueueMessage(msg);
  117. if (im_what == IM_MESSAGE_SENT)
  118. fReceiveView->ScrollToBottom();
  119. break;
  120. }
  121. case IM_ROOM_JOINED:
  122. {
  123. BMessage msg;
  124. msg.AddString("body", B_TRANSLATE("** You joined the room.\n"));
  125. _AppendOrEnqueueMessage(&msg);
  126. fReceiveView->ScrollToBottom();
  127. }
  128. case IM_ROOM_CREATED:
  129. {
  130. BMessage msg;
  131. msg.AddString("body", B_TRANSLATE("** You created the room.\n"));
  132. _AppendOrEnqueueMessage(&msg);
  133. fReceiveView->ScrollToBottom();
  134. }
  135. case IM_ROOM_PARTICIPANT_JOINED:
  136. {
  137. _UserMessage(B_TRANSLATE("%user% has joined the room.\n"),
  138. B_TRANSLATE("%user% has joined the room (%body%).\n"),
  139. msg);
  140. break;
  141. }
  142. case IM_ROOM_PARTICIPANT_LEFT:
  143. {
  144. _UserMessage(B_TRANSLATE("%user% has left the room.\n"),
  145. B_TRANSLATE("%user% has left the room (%body%).\n"),
  146. msg);
  147. break;
  148. }
  149. case IM_ROOM_PARTICIPANT_KICKED:
  150. {
  151. _UserMessage(B_TRANSLATE("%user% was kicked.\n"),
  152. B_TRANSLATE("%user% was kicked (%body%).\n"),msg);
  153. break;
  154. }
  155. case IM_ROOM_PARTICIPANT_BANNED:
  156. {
  157. _UserMessage(B_TRANSLATE("%user% has been banned.\n"),
  158. B_TRANSLATE("%user% has been banned (%body%).\n"),
  159. msg);
  160. break;
  161. }
  162. case IM_ROOM_ROLECHANGED:
  163. {
  164. BString user_id = msg->FindString("user_id");
  165. if (user_id == fConversation->GetOwnContact()->GetId()) {
  166. Role* role = fConversation->GetRole(user_id);
  167. if (role == NULL)
  168. break;
  169. int32 perms = role->fPerms;
  170. fNameTextView->MakeEditable(perms & PERM_ROOM_NAME);
  171. fSubjectTextView->MakeEditable(perms & PERM_ROOM_SUBJECT);
  172. }
  173. break;
  174. }
  175. case IM_SET_ROOM_NAME:
  176. case IM_SET_ROOM_SUBJECT:
  177. {
  178. if (fConversation == NULL)
  179. return;
  180. fConversation->GetProtocolLooper()->MessageReceived(msg);
  181. // Reset to current values; if the change went through, it'll
  182. // come back.
  183. fNameTextView->SetText(fConversation->GetName());
  184. fSubjectTextView->SetText(fConversation->GetSubject());
  185. break;
  186. }
  187. default:
  188. break;
  189. }
  190. }
  191. Conversation*
  192. ConversationView::GetConversation()
  193. {
  194. return fConversation;
  195. }
  196. void
  197. ConversationView::SetConversation(Conversation* chat)
  198. {
  199. if (chat == NULL)
  200. return;
  201. fConversation = chat;
  202. BMessage name(IM_MESSAGE);
  203. name.AddInt32("im_what", IM_SET_ROOM_NAME);
  204. name.AddString("chat_id", chat->GetId());
  205. fNameTextView->SetText(chat->GetName());
  206. fNameTextView->SetMessage(name, "chat_name");
  207. fNameTextView->SetTarget(this);
  208. BMessage subject(IM_MESSAGE);
  209. subject.AddInt32("im_what", IM_SET_ROOM_SUBJECT);
  210. subject.AddString("chat_id", chat->GetId());
  211. fSubjectTextView->SetText(chat->GetSubject());
  212. fSubjectTextView->SetMessage(subject, "subject");
  213. fSubjectTextView->SetTarget(this);
  214. fProtocolView->SetBitmap(chat->ProtocolBitmap());
  215. }
  216. void
  217. ConversationView::UpdateUserList(UserMap users)
  218. {
  219. fUserList->MakeEmpty();
  220. for (int i = 0; i < users.CountItems(); i++) {
  221. User* user = users.ValueAt(i);
  222. if (fUserList->HasUser(user) == false) {
  223. fUserList->AddUser(user);
  224. fUserList->Sort();
  225. }
  226. }
  227. }
  228. void
  229. ConversationView::InvalidateUserList()
  230. {
  231. for (int i = 0; i < fUserList->CountItems(); i++)
  232. fUserList->InvalidateItem(i);
  233. }
  234. void
  235. ConversationView::ObserveString(int32 what, BString str)
  236. {
  237. switch (what)
  238. {
  239. case STR_ROOM_NAME:
  240. {
  241. fNameTextView->SetText(str);
  242. break;
  243. }
  244. case STR_ROOM_SUBJECT:
  245. {
  246. fSubjectTextView->SetText(str);
  247. BString body = B_TRANSLATE("** The subject is now: %subject%");
  248. body.ReplaceAll("%subject%", str);
  249. BMessage topic(IM_MESSAGE);
  250. topic.AddString("body", body);
  251. _AppendOrEnqueueMessage(&topic);
  252. break;
  253. }
  254. }
  255. }
  256. void
  257. ConversationView::ObservePointer(int32 what, void* ptr)
  258. {
  259. switch (what)
  260. {
  261. case PTR_ROOM_BITMAP:
  262. {
  263. if (ptr != NULL)
  264. fIcon->SetBitmap((BBitmap*)ptr);
  265. break;
  266. }
  267. }
  268. }
  269. void
  270. ConversationView::GetWeights(float* horizChat, float* horizList,
  271. float* vertChat, float* vertSend)
  272. {
  273. *horizChat = fHorizSplit->ItemWeight(0);
  274. *horizList = fHorizSplit->ItemWeight(1);
  275. *vertChat = fVertSplit->ItemWeight(0);
  276. *vertSend = fVertSplit->ItemWeight(1);
  277. }
  278. void
  279. ConversationView::SetWeights(float horizChat, float horizList, float vertChat,
  280. float vertSend)
  281. {
  282. fHorizSplit->SetItemWeight(0, horizChat, true);
  283. fHorizSplit->SetItemWeight(1, horizList, true);
  284. fVertSplit->SetItemWeight(0, vertChat, true);
  285. fVertSplit->SetItemWeight(1, vertSend, true);
  286. }
  287. void
  288. ConversationView::_InitInterface()
  289. {
  290. fReceiveView = new RenderView("receiveView");
  291. BScrollView* scrollViewReceive = new BScrollView("receiveScrollView",
  292. fReceiveView, B_WILL_DRAW, false, true, B_NO_BORDER);
  293. fSendView = new SendTextView("sendView", this);
  294. fNameTextView = new EnterTextView("roomName", be_bold_font, NULL, B_WILL_DRAW);
  295. fNameTextView->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
  296. fNameTextView->SetStylable(true);
  297. fNameTextView->MakeEditable(false);
  298. fNameTextView->MakeResizable(true);
  299. fSubjectTextView = new EnterTextView("roomSubject");
  300. fSubjectTextView->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
  301. fSubjectTextView->MakeEditable(false);
  302. fSubjectTextView->MakeResizable(true);
  303. fIcon = new BitmapView("ContactIcon");
  304. fIcon->SetExplicitMinSize(BSize(50, 50));
  305. fIcon->SetExplicitPreferredSize(BSize(50, 50));
  306. fIcon->SetExplicitAlignment(BAlignment(B_ALIGN_RIGHT, B_ALIGN_MIDDLE));
  307. fIcon->SetSquare(true);
  308. fProtocolView = new BitmapView("protocolView");
  309. fUserList = new UserListView("userList");
  310. BScrollView* scrollViewUsers = new BScrollView("userScrollView",
  311. fUserList, B_WILL_DRAW, false, true);
  312. fHorizSplit = new BSplitView(B_HORIZONTAL, 0);
  313. fVertSplit = new BSplitView(B_VERTICAL, 0);
  314. BLayoutBuilder::Group<>(this, B_VERTICAL)
  315. .AddGroup(B_HORIZONTAL)
  316. .Add(fIcon)
  317. .AddGroup(B_VERTICAL)
  318. .Add(fNameTextView)
  319. .Add(fSubjectTextView)
  320. .End()
  321. .Add(fProtocolView)
  322. .End()
  323. .AddSplit(fHorizSplit, 0.0)
  324. .AddGroup(B_VERTICAL)
  325. .AddSplit(fVertSplit, 8.0)
  326. .Add(scrollViewReceive, 20)
  327. .Add(fSendView, 1)
  328. .End()
  329. .End()
  330. .Add(scrollViewUsers, 1)
  331. .End()
  332. .End();
  333. }
  334. bool
  335. ConversationView::_AppendOrEnqueueMessage(BMessage* msg)
  336. {
  337. if (msg->HasInt64("when") == false)
  338. msg->AddInt64("when", (int64)time(NULL));
  339. // If not attached to the chat window, then re-handle this message
  340. // later [AttachedToWindow()], since you can't edit an unattached
  341. // RenderView.
  342. if (Window() == NULL) {
  343. // If contains multiple chat messages (e.g., IM_LOGS_RECEIVED), add all
  344. int32 i = -1;
  345. BMessage text;
  346. while (msg->FindMessage("message", i + 1, &text) == B_OK) {
  347. fMessageQueue.AddItem(new BMessage(text));
  348. i++;
  349. }
  350. // Else, add the lonely, lonely, single-messaged one
  351. if (i == -1)
  352. fMessageQueue.AddItem(new BMessage(*msg));
  353. return false;
  354. }
  355. // Alright, we're good to append!
  356. _AppendMessage(msg);
  357. return true;
  358. }
  359. void
  360. ConversationView::_AppendMessage(BMessage* msg)
  361. {
  362. // If ordered to clear buffer… well, I guess we can't refuse
  363. if (msg->what == kClearText) {
  364. fReceiveView->SetText("");
  365. return;
  366. }
  367. // Otherwise, it's message time!
  368. int64 timeInt;
  369. BString user_id;
  370. BString user_name = msg->FindString("user_name");
  371. BString body;
  372. rgb_color userColor = ui_color(B_PANEL_TEXT_COLOR);
  373. if (msg->FindString("body", &body) != B_OK)
  374. return;
  375. if (msg->FindInt64("when", &timeInt) != B_OK)
  376. timeInt = (int64)time(NULL);
  377. if (msg->FindString("user_id", &user_id) == B_OK) {
  378. User* user = NULL;
  379. if (fConversation != NULL
  380. && (user = fConversation->UserById(user_id)) != NULL) {
  381. user_name = user->GetName();
  382. userColor = user->fItemColor;
  383. }
  384. else if (user_name.IsEmpty() == true)
  385. user_name = user_id;
  386. }
  387. if (user_name.IsEmpty() == true) {
  388. fReceiveView->AppendGeneric(body);
  389. return;
  390. }
  391. if (body.StartsWith("/me ")) {
  392. BString meMsg = "** ";
  393. meMsg << user_name.String() << " ";
  394. meMsg << body.RemoveFirst("/me ");
  395. fReceiveView->AppendGeneric(meMsg.String());
  396. return;
  397. }
  398. fReceiveView->AppendTimestamp(timeInt);
  399. fReceiveView->AppendUserstamp(user_name, userColor);
  400. // And here we append the body…
  401. uint16 face = 0;
  402. UInt16IntMap face_indices;
  403. rgb_color color = ui_color(B_PANEL_TEXT_COLOR);
  404. int32 colorIndice = -1;
  405. int32 next = body.CountChars();
  406. BFont font;
  407. for (int i = 0; i < body.CountChars(); i++) {
  408. _DisableEndingFaces(msg, i, &face, &face_indices);
  409. _EnableStartingFaces(msg, i, &face, &face_indices, &next);
  410. _EnableStartingColor(msg, i, &color, &colorIndice, &next);
  411. if (face == B_REGULAR_FACE) {
  412. font = BFont();
  413. face = 0;
  414. }
  415. else if (face > 0) {
  416. font.SetFace(face);
  417. }
  418. if (colorIndice > 0 && colorIndice <= i) {
  419. color = ui_color(B_PANEL_TEXT_COLOR);
  420. colorIndice == -1;
  421. }
  422. // If formatting doesn't change for a while, send text in bulk
  423. if ((next - i) > 1) {
  424. BString append;
  425. body.CopyCharsInto(append, i, next - i);
  426. fReceiveView->Append(append, color, &font);
  427. i = next - 1;
  428. next = body.CountChars();
  429. }
  430. // Otherwise, send only current character
  431. else {
  432. int32 bytes;
  433. const char* curChar = body.CharAt(i, &bytes);
  434. char append[bytes];
  435. for (int i = 0; i < bytes; i++)
  436. append[i] = curChar[i];
  437. append[bytes] = '\0';
  438. fReceiveView->Append(append, color, &font);
  439. }
  440. if (next < i)
  441. next = body.CountChars() - 1;
  442. }
  443. fReceiveView->Append("\n");
  444. }
  445. void
  446. ConversationView::_EnableStartingFaces(BMessage* msg, int32 index, uint16* face,
  447. UInt16IntMap* indices, int32* next)
  448. {
  449. int32 face_start;
  450. int32 face_length;
  451. uint16 newFace;
  452. int32 i = 0;
  453. while (msg->FindInt32("face_start", i, &face_start) == B_OK) {
  454. // Change 'next' value for new fonts
  455. if (face_start > index && face_start < *next)
  456. *next = face_start;
  457. // Set face normally
  458. if (face_start == index) {
  459. if (msg->FindInt32("face_length", i, &face_length) != B_OK)
  460. continue;
  461. if (msg->FindUInt16("face", i, &newFace) != B_OK)
  462. continue;
  463. *face |= newFace;
  464. indices->AddItem(newFace, index + face_length);
  465. }
  466. i++;
  467. }
  468. // Change 'next' for ending old fonts
  469. for (int i = 0; i < indices->CountItems(); i++) {
  470. int faceEnd = indices->ValueAt(i);
  471. if (faceEnd > 0 && faceEnd > index && faceEnd < *next)
  472. *next = faceEnd;
  473. }
  474. }
  475. void
  476. ConversationView::_DisableEndingFaces(BMessage* msg, int32 index, uint16* face,
  477. UInt16IntMap* indices)
  478. {
  479. for (int32 i = 0; i < indices->CountItems(); i++) {
  480. uint16 key = indices->KeyAt(i);
  481. int32 value = indices->ValueAt(i);
  482. if (value <= index) {
  483. indices->RemoveItemAt(i);
  484. *face = *face & ~key;
  485. if (indices->CountItems() == 0)
  486. *face = B_REGULAR_FACE;
  487. }
  488. }
  489. }
  490. void
  491. ConversationView::_EnableStartingColor(BMessage* msg, int32 index,
  492. rgb_color* color, int32* indice, int32* next)
  493. {
  494. rgb_color newColor;
  495. int32 color_start, color_length, i = 0;
  496. while (msg->FindInt32("color_start", i, &color_start) == B_OK) {
  497. if (color_start > index && color_start < *next)
  498. *next = color_start - 1;
  499. if (color_start == index
  500. && msg->FindInt32("color_length", i, &color_length) == B_OK
  501. && msg->FindColor("color", i, &newColor) == B_OK)
  502. {
  503. *indice = color_length + index;
  504. *color = newColor;
  505. if (*indice > index && (*indice < *next || *next < index))
  506. *next = *indice;
  507. break;
  508. }
  509. i++;
  510. }
  511. }
  512. void
  513. ConversationView::_UserMessage(const char* format, const char* bodyFormat,
  514. BMessage* msg)
  515. {
  516. BString user_id;
  517. BString user_name = msg->FindString("user_name");
  518. BString body = msg->FindString("body");
  519. if (msg->FindString("user_id", &user_id) != B_OK)
  520. return;
  521. if (user_name.IsEmpty() == true)
  522. user_name = user_id;
  523. BString newBody("** ");
  524. if (body.IsEmpty() == true)
  525. newBody << format;
  526. else {
  527. newBody << bodyFormat;
  528. newBody.ReplaceAll("%body%", body.String());
  529. }
  530. newBody.ReplaceAll("%user%", user_name.String());
  531. BMessage newMsg;
  532. newMsg.AddString("body", newBody);
  533. _AppendOrEnqueueMessage(&newMsg);
  534. fReceiveView->ScrollToBottom();
  535. }
  536. #undef B_TRANSLATION_CONTEXT
  537. #define B_TRANSLATION_CONTEXT "ConversationView ― Startup messages"
  538. void
  539. ConversationView::_FakeChat()
  540. {
  541. BString name(B_TRANSLATE("%app% setup"));
  542. name.ReplaceAll("%app%", APP_NAME);
  543. fNameTextView->SetText(name);
  544. fSubjectTextView->SetText(B_TRANSLATE("No accounts enabled, no joy."));
  545. BMessage obsv(IM_MESSAGE);
  546. obsv.AddInt32("im_what", IM_MESSAGE_RECEIVED);
  547. obsv.AddString("user_name", B_TRANSLATE("Master Foo"));
  548. obsv.AddString("body", B_TRANSLATE("It looks like you don't have any accounts enabled."));
  549. _AppendOrEnqueueMessage(&obsv);
  550. BString body = B_TRANSLATE("Manage accounts through the %menu% menu to get started.");
  551. BString menu = B_TRANSLATE("Accounts");
  552. int32 boldStart = body.FindFirst("%");
  553. int32 boldLength = menu.CountChars();
  554. body.ReplaceAll("%menu%", menu);
  555. BMessage add(IM_MESSAGE);
  556. add.AddInt32("im_what", IM_MESSAGE_RECEIVED);
  557. add.AddString("user_name", B_TRANSLATE("Master Foo"));
  558. add.AddString("body", body);
  559. add.AddInt32("face_start", boldStart);
  560. add.AddInt32("face_length", boldLength);
  561. add.AddUInt16("face", B_BOLD_FACE);
  562. _AppendOrEnqueueMessage(&add);
  563. body = B_TRANSLATE("Afterward, you can join a room or start a chat through the %menu% menu. :-)");
  564. menu = B_TRANSLATE("Chat");
  565. boldStart = body.FindFirst("%");
  566. boldLength = menu.CountChars();
  567. body.ReplaceAll("%menu%", menu);
  568. BMessage welcome(IM_MESSAGE);
  569. welcome.AddInt32("im_what", IM_MESSAGE_RECEIVED);
  570. welcome.AddString("user_name", B_TRANSLATE("Master Foo"));
  571. welcome.AddString("body", body);
  572. welcome.AddInt32("face_start", boldStart);
  573. welcome.AddInt32("face_length", boldLength);
  574. welcome.AddUInt16("face", B_BOLD_FACE);
  575. _AppendOrEnqueueMessage(&welcome);
  576. }