1 /*
2 * The ManaPlus Client
3 * Copyright (C) 2008-2009 The Mana World Development Team
4 * Copyright (C) 2009-2010 The Mana Developers
5 * Copyright (C) 2011-2019 The ManaPlus Developers
6 * Copyright (C) 2019-2021 Andrei Karas
7 *
8 * This file is part of The ManaPlus Client.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 */
23
24 #include "gui/widgets/tabs/chat/chattab.h"
25
26 #include "chatlogger.h"
27 #include "configuration.h"
28 #include "settings.h"
29 #include "soundmanager.h"
30
31 #include "being/localplayer.h"
32
33 #include "const/sound.h"
34
35 #include "gui/chatlog.h"
36 #include "gui/windowmanager.h"
37
38 #include "gui/windows/chatwindow.h"
39 #include "gui/windows/helpwindow.h"
40
41 #include "gui/widgets/scrollarea.h"
42 #include "gui/widgets/itemlinkhandler.h"
43 #include "gui/widgets/tabbedarea.h"
44
45 #include "input/inputmanager.h"
46
47 #include "net/chathandler.h"
48 #include "net/net.h"
49
50 #include "utils/chatutils.h"
51 #include "utils/delete2.h"
52 #include "utils/gettext.h"
53
54 #ifdef WIN32
55 #include <sys/time.h>
56 #endif // WIN32
57
58 #include <sstream>
59
60 #include "debug.h"
61
62 ChatTab *localChatTab = nullptr;
63 ChatTab *debugChatTab = nullptr;
64
65 static const unsigned int MAX_WORD_SIZE = 50;
66
ChatTab(const Widget2 * const widget,const std::string & name,const std::string & channel,const std::string & logName,const ChatTabTypeT & type)67 ChatTab::ChatTab(const Widget2 *const widget,
68 const std::string &name,
69 const std::string &channel,
70 const std::string &logName,
71 const ChatTabTypeT &type) :
72 Tab(widget),
73 mTextOutput(new BrowserBox(this, Opaque_true,
74 "browserbox.xml")),
75 mScrollArea(new ScrollArea(this,
76 mTextOutput, Opaque_false, std::string())),
77 mChannelName(channel),
78 mLogName(logName),
79 mType(type),
80 mAllowHightlight(true),
81 mRemoveNames(false),
82 mNoAway(false),
83 mShowOnline(false)
84 {
85 setCaption(name);
86
87 mTextOutput->setOpaque(Opaque_false);
88 mTextOutput->setMaxRow(config.getIntValue("ChatLogLength"));
89 if (chatWindow != nullptr)
90 mTextOutput->setLinkHandler(chatWindow->mItemLinkHandler);
91 mTextOutput->setAlwaysUpdate(false);
92
93 mScrollArea->setScrollPolicy(ScrollArea::SHOW_NEVER,
94 ScrollArea::SHOW_ALWAYS);
95 mScrollArea->setScrollAmount(0, 1);
96
97 if (chatWindow != nullptr)
98 chatWindow->addTab(this);
99 mTextOutput->updateSize(true);
100 }
101
~ChatTab()102 ChatTab::~ChatTab()
103 {
104 if (chatWindow != nullptr)
105 chatWindow->removeTab(this);
106
107 delete2(mTextOutput)
108 delete2(mScrollArea)
109 }
110
chatLog(std::string line,ChatMsgTypeT own,const IgnoreRecord ignoreRecord,const TryRemoveColors tryRemoveColors)111 void ChatTab::chatLog(std::string line,
112 ChatMsgTypeT own,
113 const IgnoreRecord ignoreRecord,
114 const TryRemoveColors tryRemoveColors)
115 {
116 // Trim whitespace
117 trim(line);
118
119 if (line.empty())
120 return;
121
122 if (tryRemoveColors == TryRemoveColors_true &&
123 own == ChatMsgType::BY_OTHER &&
124 config.getBoolValue("removeColors"))
125 {
126 line = removeColors(line);
127 if (line.empty())
128 return;
129 }
130
131 const unsigned lineLim = config.getIntValue("chatMaxCharLimit");
132 if (lineLim > 0 && line.length() > lineLim)
133 line = line.substr(0, lineLim);
134
135 if (line.empty())
136 return;
137
138 CHATLOG tmp;
139 tmp.own = own;
140 tmp.nick.clear();
141 tmp.text = line;
142
143 const size_t pos = line.find(" : ");
144 if (pos != std::string::npos)
145 {
146 if (line.length() <= pos + 3)
147 return;
148
149 tmp.nick = line.substr(0, pos);
150 tmp.text = line.substr(pos + 3);
151 }
152 else
153 {
154 // Fix the owner of welcome message.
155 if (line.length() > 7 && line.substr(0, 7) == "Welcome")
156 own = ChatMsgType::BY_SERVER;
157 }
158
159 // *implements actions in a backwards compatible way*
160 if ((own == ChatMsgType::BY_PLAYER || own == ChatMsgType::BY_OTHER) &&
161 tmp.text.at(0) == '*' &&
162 tmp.text.at(tmp.text.length()-1) == '*')
163 {
164 tmp.text[0] = ' ';
165 tmp.text.erase(tmp.text.length() - 1);
166 own = ChatMsgType::ACT_IS;
167 }
168
169 std::string lineColor("##C");
170 switch (own)
171 {
172 case ChatMsgType::BY_GM:
173 if (tmp.nick.empty())
174 {
175 // TRANSLATORS: chat message
176 tmp.nick = std::string(_("Global announcement:")).append(" ");
177 lineColor = "##G";
178 }
179 else
180 {
181 // TRANSLATORS: chat message
182 tmp.nick = strprintf(_("Global announcement from %s:"),
183 tmp.nick.c_str()).append(" ");
184 lineColor = "##g"; // Equiv. to BrowserBox::RED
185 }
186 break;
187 case ChatMsgType::BY_PLAYER:
188 tmp.nick.append(": ");
189 lineColor = "##Y";
190 break;
191 case ChatMsgType::BY_OTHER:
192 case ChatMsgType::BY_UNKNOWN:
193 tmp.nick.append(": ");
194 lineColor = "##C";
195 break;
196 case ChatMsgType::BY_SERVER:
197 // TRANSLATORS: chat message
198 tmp.nick.clear();
199 tmp.text = line;
200 lineColor = "##S";
201 break;
202 case ChatMsgType::BY_CHANNEL:
203 tmp.nick.clear();
204 lineColor = "##2"; // Equiv. to BrowserBox::GREEN
205 break;
206 case ChatMsgType::ACT_WHISPER:
207 // TRANSLATORS: chat message
208 tmp.nick = strprintf(_("%s whispers: %s"), tmp.nick.c_str(), "");
209 lineColor = "##W";
210 break;
211 case ChatMsgType::ACT_IS:
212 lineColor = "##I";
213 break;
214 case ChatMsgType::BY_LOGGER:
215 tmp.nick.clear();
216 tmp.text = line;
217 lineColor = "##L";
218 break;
219 default:
220 break;
221 }
222
223 if (tmp.nick == ": ")
224 {
225 tmp.nick.clear();
226 lineColor = "##S";
227 }
228
229 // if configured, move magic messages log to debug chat tab
230 if (Net::getNetworkType() == ServerType::TMWATHENA
231 && (localChatTab != nullptr) && this == localChatTab
232 && ((config.getBoolValue("showMagicInDebug")
233 && own == ChatMsgType::BY_PLAYER
234 && tmp.text.length() > 1
235 && tmp.text.at(0) == '#'
236 && tmp.text.at(1) != '#')
237 || (config.getBoolValue("serverMsgInDebug")
238 && (own == ChatMsgType::BY_SERVER
239 || tmp.nick.empty()))))
240 {
241 if (debugChatTab != nullptr)
242 debugChatTab->chatLog(line, own, ignoreRecord, tryRemoveColors);
243 return;
244 }
245
246 // Get the current system time
247 time_t t;
248 time(&t);
249
250 if (config.getBoolValue("useLocalTime"))
251 {
252 const tm *const timeInfo = localtime(&t);
253 if (timeInfo != nullptr)
254 {
255 line = strprintf("%s[%02d:%02d] %s%s", lineColor.c_str(),
256 timeInfo->tm_hour, timeInfo->tm_min, tmp.nick.c_str(),
257 tmp.text.c_str());
258 }
259 else
260 {
261 line = strprintf("%s %s%s", lineColor.c_str(),
262 tmp.nick.c_str(), tmp.text.c_str());
263 }
264 }
265 else
266 {
267 // Format the time string properly
268 std::stringstream timeStr;
269 timeStr << "[" << ((((t / 60) / 60) % 24 < 10) ? "0" : "")
270 << CAST_S32(((t / 60) / 60) % 24)
271 << ":" << (((t / 60) % 60 < 10) ? "0" : "")
272 << CAST_S32((t / 60) % 60)
273 << "] ";
274 line = std::string(lineColor).append(timeStr.str()).append(
275 tmp.nick).append(tmp.text);
276 }
277
278 if (config.getBoolValue("enableChatLog"))
279 saveToLogFile(line);
280
281 mTextOutput->setMaxRow(config.getIntValue("chatMaxLinesLimit"));
282
283 // We look if the Vertical Scroll Bar is set at the max before
284 // adding a row, otherwise the max will always be a row higher
285 // at comparison.
286 if (mScrollArea->getVerticalScrollAmount() + 2 >=
287 mScrollArea->getVerticalMaxScroll())
288 {
289 addRow(line);
290 mScrollArea->setVerticalScrollAmount(
291 mScrollArea->getVerticalMaxScroll());
292 }
293 else
294 {
295 addRow(line);
296 }
297
298 if ((chatWindow != nullptr) && this == localChatTab)
299 chatWindow->addToAwayLog(line);
300
301 mScrollArea->logic();
302 if (own != ChatMsgType::BY_PLAYER)
303 {
304 if (own == ChatMsgType::BY_SERVER &&
305 (getType() == ChatTabType::PARTY ||
306 getType() == ChatTabType::CHANNEL ||
307 getType() == ChatTabType::GUILD))
308 {
309 return;
310 }
311
312 const TabbedArea *const tabArea = getTabbedArea();
313 if (tabArea == nullptr)
314 return;
315
316 const bool notFocused = WindowManager::getIsMinimized() ||
317 (!settings.mouseFocused &&
318 settings.inputFocused == KeyboardFocus::Unfocused);
319
320 if (this != tabArea->getSelectedTab() || notFocused)
321 {
322 if (getFlash() == 0)
323 {
324 if (chatWindow != nullptr &&
325 chatWindow->findHighlight(tmp.text))
326 {
327 setFlash(2);
328 soundManager.playGuiSound(SOUND_HIGHLIGHT);
329 }
330 else
331 {
332 setFlash(1);
333 }
334 }
335 else if (getFlash() == 2)
336 {
337 if (chatWindow != nullptr &&
338 chatWindow->findHighlight(tmp.text))
339 {
340 soundManager.playGuiSound(SOUND_HIGHLIGHT);
341 }
342 }
343 }
344
345 if ((getAllowHighlight() ||
346 own == ChatMsgType::BY_GM) &&
347 (this != tabArea->getSelectedTab() ||
348 notFocused))
349 {
350 if (own == ChatMsgType::BY_GM)
351 {
352 if (chatWindow != nullptr)
353 chatWindow->unHideWindow();
354 soundManager.playGuiSound(SOUND_GLOBAL);
355 }
356 else if (own != ChatMsgType::BY_SERVER)
357 {
358 if (chatWindow != nullptr)
359 chatWindow->unHideWindow();
360 playNewMessageSound();
361 }
362 WindowManager::newChatMessage();
363 }
364 }
365 }
366
chatLog(const std::string & nick,std::string msg)367 void ChatTab::chatLog(const std::string &nick, std::string msg)
368 {
369 if (localPlayer == nullptr)
370 return;
371
372 const ChatMsgTypeT byWho = (nick == localPlayer->getName()
373 ? ChatMsgType::BY_PLAYER : ChatMsgType::BY_OTHER);
374 if (byWho == ChatMsgType::BY_OTHER && config.getBoolValue("removeColors"))
375 msg = removeColors(msg);
376 chatLog(std::string(nick).append(" : ").append(msg),
377 byWho,
378 IgnoreRecord_false,
379 TryRemoveColors_false);
380 }
381
chatInput(const std::string & message)382 void ChatTab::chatInput(const std::string &message)
383 {
384 std::string msg = message;
385 trim(msg);
386
387 if (msg.empty())
388 return;
389
390 replaceItemLinks(msg);
391 replaceVars(msg);
392
393 switch (msg[0])
394 {
395 case '/':
396 handleCommandStr(std::string(msg, 1));
397 break;
398 case '?':
399 if (msg.size() > 1 &&
400 msg[1] != '!' &&
401 msg[1] != '?' &&
402 msg[1] != '.' &&
403 msg[1] != ' ' &&
404 msg[1] != ',')
405 {
406 handleHelp(std::string(msg, 1));
407 }
408 else
409 {
410 handleInput(msg);
411 }
412 break;
413 default:
414 handleInput(msg);
415 break;
416 }
417 }
418
scroll(const int amount)419 void ChatTab::scroll(const int amount)
420 {
421 const int range = mScrollArea->getHeight() / 8 * amount;
422 Rect scr;
423 scr.y = mScrollArea->getVerticalScrollAmount() + range;
424 scr.height = abs(range);
425 mTextOutput->showPart(scr);
426 }
427
clearText()428 void ChatTab::clearText()
429 {
430 mTextOutput->clearRows();
431 }
432
handleInput(const std::string & msg)433 void ChatTab::handleInput(const std::string &msg)
434 {
435 if (chatHandler)
436 {
437 chatHandler->talk(ChatWindow::doReplace(msg));
438 }
439 }
440
handleCommandStr(const std::string & msg)441 void ChatTab::handleCommandStr(const std::string &msg)
442 {
443 const size_t pos = msg.find(' ');
444 const std::string type(msg, 0, pos);
445 std::string args(msg, pos == std::string::npos ? msg.size() : pos + 1);
446
447 args = trim(args);
448 if (!handleCommand(type, args))
449 inputManager.executeChatCommand(type, args, this);
450 }
451
handleHelp(const std::string & msg)452 void ChatTab::handleHelp(const std::string &msg)
453 {
454 if (helpWindow != nullptr)
455 {
456 helpWindow->search(msg);
457 helpWindow->requestMoveToTop();
458 }
459 }
460
handleCommands(const std::string & type,const std::string & args)461 bool ChatTab::handleCommands(const std::string &type, const std::string &args)
462 {
463 // need split to commands and call each
464
465 return handleCommand(type, args);
466 }
467
saveToLogFile(const std::string & msg) const468 void ChatTab::saveToLogFile(const std::string &msg) const
469 {
470 if (chatLogger != nullptr)
471 {
472 if (getType() == ChatTabType::INPUT)
473 {
474 chatLogger->log(msg);
475 }
476 else if (getType() == ChatTabType::DEBUG)
477 {
478 if (config.getBoolValue("enableDebugLog"))
479 chatLogger->log("#Debug", msg);
480 }
481 else if (!mLogName.empty())
482 {
483 chatLogger->log(mLogName, msg);
484 }
485 }
486 }
487
addRow(std::string & line)488 void ChatTab::addRow(std::string &line)
489 {
490 if (line.find("[@@http") == std::string::npos)
491 {
492 size_t idx = 0;
493 for (size_t f = 0; f < line.length(); f++)
494 {
495 if (line.at(f) == ' ')
496 {
497 idx = f;
498 }
499 else if (f - idx > MAX_WORD_SIZE)
500 {
501 line.insert(f, " ");
502 idx = f;
503 }
504 }
505 }
506 mTextOutput->addRow(line,
507 false);
508 }
509
loadFromLogFile(const std::string & name)510 void ChatTab::loadFromLogFile(const std::string &name)
511 {
512 if (chatLogger != nullptr)
513 {
514 std::list<std::string> list;
515 chatLogger->loadLast(name, list, 5);
516 std::list<std::string>::const_iterator i = list.begin();
517 while (i != list.end())
518 {
519 std::string line("##o" + *i);
520 addRow(line);
521 ++i;
522 }
523 }
524 }
525
addNewRow(std::string & line)526 void ChatTab::addNewRow(std::string &line)
527 {
528 if (mScrollArea->getVerticalScrollAmount() >=
529 mScrollArea->getVerticalMaxScroll())
530 {
531 addRow(line);
532 mScrollArea->setVerticalScrollAmount(
533 mScrollArea->getVerticalMaxScroll());
534 }
535 else
536 {
537 addRow(line);
538 }
539 mScrollArea->logic();
540 }
541
playNewMessageSound() const542 void ChatTab::playNewMessageSound() const
543 {
544 soundManager.playGuiSound(SOUND_WHISPER);
545 }
546
showOnline(const std::string & nick,const Online online)547 void ChatTab::showOnline(const std::string &nick,
548 const Online online)
549 {
550 if (!mShowOnline)
551 return;
552
553 if (online == Online_true)
554 {
555 // TRANSLATORS: chat message
556 chatLog(strprintf(_("%s is now Online."), nick.c_str()),
557 ChatMsgType::BY_SERVER,
558 IgnoreRecord_false,
559 TryRemoveColors_true);
560 }
561 else
562 {
563 // TRANSLATORS: chat message
564 chatLog(strprintf(_("%s is now Offline."), nick.c_str()),
565 ChatMsgType::BY_SERVER,
566 IgnoreRecord_false,
567 TryRemoveColors_true);
568 }
569 }
570