/*
Copyright (c) 2005 Victor Pavlychko (nullbyte@sotline.net.ua)
Copyright (C) 2012-24 Miranda NG team (https://miranda-ng.org)
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation version 2
of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
#include "stdafx.h"
/////////////////////////////////////////////////////////////////////////////////////////
// Filters
bool Filter::check(ItemData *item) const
{
if (!item) return false;
if (!(flags & EVENTONLY)) {
if (item->dbe.flags & DBEF_SENT) {
if (!(flags & OUTGOING))
return false;
}
else {
if (!(flags & INCOMING))
return false;
}
switch (item->dbe.eventType) {
case EVENTTYPE_MESSAGE:
if (!(flags & MESSAGES))
return false;
break;
case EVENTTYPE_FILE:
if (!(flags & FILES))
return false;
break;
case EVENTTYPE_STATUSCHANGE:
if (!(flags & STATUS))
return false;
break;
default:
if (!(flags & OTHER))
return false;
}
}
if (flags & (EVENTTEXT | EVENTONLY)) {
if (item->m_bLoaded)
return CheckFilter(item->wtext, text);
if (!item->fetch())
return false;
return CheckFilter(ptrW(DbEvent_GetTextW(&item->dbe)), text);
}
return true;
};
/////////////////////////////////////////////////////////////////////////////////////////
// Event
ItemData::ItemData()
{
memset(this, 0, sizeof(*this));
savedHeight = -1;
}
ItemData::~ItemData()
{
mir_free(qtext);
mir_free(wtext);
if (dbe.szReplyId)
mir_free((char*)dbe.szReplyId);
}
/////////////////////////////////////////////////////////////////////////////////////////
static bool isEqual(const ItemData *p1, const ItemData *p2)
{
if (p1->hContact != p2->hContact)
return false;
if (p1->dbe.eventType != p2->dbe.eventType)
return false;
if ((p1->dbe.flags & DBEF_SENT) != (p2->dbe.flags & DBEF_SENT))
return false;
if (p1->dbe.timestamp / 86400 != p2->dbe.timestamp / 86400)
return false;
return true;
}
ItemData* ItemData::checkPrev(ItemData *pPrev)
{
m_grouping = GROUPING_NONE;
if (!pPrev || !g_plugin.bMsgGrouping)
return this;
// we don't group anything but messages
if (!fetch())
return this;
if (dbe.eventType != EVENTTYPE_MESSAGE)
return this;
if (isEqual(this, pPrev)) {
if (pPrev->m_grouping == GROUPING_NONE) {
pPrev->m_grouping = GROUPING_HEAD;
if (pPrev->m_bLoaded)
pPrev->setText();
}
m_grouping = GROUPING_ITEM;
}
return this;
}
/////////////////////////////////////////////////////////////////////////////////////////
ItemData* ItemData::checkNext(ItemData *pPrev)
{
m_grouping = GROUPING_NONE;
if (!pPrev || !g_plugin.bMsgGrouping)
return this;
// we don't group anything but messages
if (!fetch())
return this;
if (dbe.eventType != EVENTTYPE_MESSAGE)
return this;
pPrev->fetch();
if (isEqual(this, pPrev)) {
if (pPrev->m_grouping == GROUPING_NONE) {
pPrev->m_grouping = GROUPING_HEAD;
if (pPrev->m_bLoaded)
pPrev->setText();
}
m_grouping = GROUPING_ITEM;
if (m_bLoaded)
setText();
}
return this;
}
/////////////////////////////////////////////////////////////////////////////////////////
static bool isEqualGC(const ItemData *p1, const ItemData *p2)
{
if (p1->dbe.eventType != p2->dbe.eventType)
return false;
if (!p1->wszNick || !p2->wszNick)
return false;
if (wcscmp(p1->wszNick, p2->wszNick))
return false;
if (p1->dbe.timestamp / 86400 != p2->dbe.timestamp / 86400)
return false;
return true;
}
ItemData* ItemData::checkPrevGC(ItemData *pPrev)
{
m_grouping = GROUPING_NONE;
if (!pPrev || !g_plugin.bMsgGrouping)
return this;
if (dbe.eventType != EVENTTYPE_MESSAGE)
return this;
if (isEqualGC(this, pPrev)) {
if (pPrev->m_grouping == GROUPING_NONE) {
pPrev->m_grouping = GROUPING_HEAD;
if (pPrev->m_bLoaded)
pPrev->setText();
}
m_grouping = GROUPING_ITEM;
}
return this;
}
/////////////////////////////////////////////////////////////////////////////////////////
int ItemData::calcHeight(int top, int width, POINT *pPos)
{
if (m_doc == nullptr)
setText();
SIZE sz;
sz.cx = width - 2;
POINT pos;
pos.x = 2;
pos.y = top + 2;
if (!pOwner->bReadOnly) {
if (g_plugin.bShowType) // Message type icon
pos.x += 18;
if (g_plugin.bShowDirection) // Message direction icon
pos.x += 18;
if (dbe.flags & DBEF_BOOKMARK) // Bookmark icon
pos.x += 18;
sz.cx -= pos.x;
if (m_bOfflineDownloaded != 0) // Download completed icon
sz.cx -= 18;
}
leftOffset = pos.x;
if (savedHeight == -1) {
m_doc->render(sz.cx);
savedHeight = m_doc->height() + 5;
}
if (pPos)
*pPos = pos;
return savedHeight;
}
bool ItemData::fetch(void)
{
// if this event is virtual (for example, in group chats), don't try to laod it
if (!dbe.getEvent())
return false;
if (!dbe) {
if (!dbe.fetch())
return false;
if (dbe.szReplyId)
dbe.szReplyId = mir_strdup(dbe.szReplyId);
}
return true;
}
void ItemData::fill(int tmpl)
{
switch (tmpl) {
case TPL_MESSAGE:
dbe.eventType = EVENTTYPE_MESSAGE;
break;
case TPL_MSG_HEAD:
m_grouping = GROUPING_HEAD;
dbe.eventType = EVENTTYPE_MESSAGE;
break;
case TPL_MSG_GRP:
m_grouping = GROUPING_ITEM;
dbe.eventType = EVENTTYPE_MESSAGE;
break;
case TPL_FILE:
dbe.eventType = EVENTTYPE_FILE;
break;
case TPL_SIGN:
dbe.eventType = EVENTTYPE_STATUSCHANGE;
break;
case TPL_AUTH:
dbe.eventType = EVENTTYPE_AUTHREQUEST;
break;
case TPL_ADDED:
dbe.eventType = EVENTTYPE_ADDED;
break;
case TPL_PRESENCE:
dbe.eventType = EVENTTYPE_JABBER_PRESENCE;
break;
default:
dbe.eventType = 9247;
break;
}
}
int ItemData::getTemplate() const
{
switch (dbe.eventType) {
case EVENTTYPE_MESSAGE:
switch (m_grouping) {
case GROUPING_HEAD: return TPL_MSG_HEAD;
case GROUPING_ITEM: return TPL_MSG_GRP;
}
return TPL_MESSAGE;
case EVENTTYPE_FILE: return TPL_FILE;
case EVENTTYPE_STATUSCHANGE: return TPL_SIGN;
case EVENTTYPE_AUTHREQUEST: return TPL_AUTH;
case EVENTTYPE_ADDED: return TPL_ADDED;
case EVENTTYPE_JABBER_PRESENCE: return TPL_PRESENCE;
default:
return TPL_OTHER;
}
}
int ItemData::getCopyTemplate() const
{
switch (dbe.eventType) {
case EVENTTYPE_MESSAGE: return TPL_COPY_MESSAGE;
case EVENTTYPE_FILE: return TPL_COPY_FILE;
case EVENTTYPE_STATUSCHANGE: return TPL_COPY_SIGN;
case EVENTTYPE_AUTHREQUEST: return TPL_COPY_AUTH;
case EVENTTYPE_ADDED: return TPL_COPY_ADDED;
case EVENTTYPE_JABBER_PRESENCE: return TPL_COPY_PRESENCE;
default:
return TPL_COPY_OTHER;
}
}
void ItemData::getFontColor(int &fontId, int &colorId) const
{
switch (dbe.eventType) {
case EVENTTYPE_MESSAGE:
fontId = !(dbe.flags & DBEF_SENT) ? FONT_INMSG : FONT_OUTMSG;
colorId = !(dbe.flags & DBEF_SENT) ? COLOR_INMSG : COLOR_OUTMSG;
break;
case EVENTTYPE_FILE:
fontId = !(dbe.flags & DBEF_SENT) ? FONT_INFILE : FONT_OUTFILE;
colorId = !(dbe.flags & DBEF_SENT) ? COLOR_INFILE : COLOR_OUTFILE;
break;
case EVENTTYPE_STATUSCHANGE:
fontId = FONT_STATUS;
colorId = COLOR_STATUS;
break;
case EVENTTYPE_AUTHREQUEST:
fontId = FONT_INOTHER;
colorId = COLOR_INOTHER;
break;
case EVENTTYPE_ADDED:
fontId = FONT_INOTHER;
colorId = COLOR_INOTHER;
break;
case EVENTTYPE_JABBER_PRESENCE:
fontId = !(dbe.flags & DBEF_SENT) ? FONT_INOTHER : FONT_OUTOTHER;
colorId = !(dbe.flags & DBEF_SENT) ? COLOR_INOTHER : COLOR_OUTOTHER;
break;
default:
fontId = !(dbe.flags & DBEF_SENT) ? FONT_INOTHER : FONT_OUTOTHER;
colorId = !(dbe.flags & DBEF_SENT) ? COLOR_INOTHER : COLOR_OUTOTHER;
break;
}
}
const char* ItemData::getUrl() const
{
if (auto el = m_doc->get_over_element())
if (!mir_strcmp(el->get_tagName(), "a"))
return el->get_attr("href");
return nullptr;
}
void ItemData::load(bool bLoadAlways)
{
if (!bLoadAlways && m_bLoaded)
return;
if (!fetch())
return;
m_bLoaded = true;
hContact = dbe.hContact; // save true contact
switch (dbe.eventType) {
case EVENTTYPE_MESSAGE:
pOwner->MarkRead(this);
__fallthrough;
case EVENTTYPE_STATUSCHANGE:
wtext = mir_utf8decodeW((char *)dbe.pBlob);
break;
case EVENTTYPE_FILE:
{
DB::FILE_BLOB blob(dbe);
if (blob.isOffline()) {
CMStringW buf;
m_bOfflineFile = true;
if (blob.isCompleted()) {
m_bOfflineDownloaded = 100;
auto *pwszLocalName = blob.getLocalName();
if (ProtoGetAvatarFileFormat(pwszLocalName) != PA_FORMAT_UNKNOWN)
buf.AppendFormat(L"[img]file://%s[/img]", pwszLocalName);
else
buf.AppendFormat(L"[url]file://%s[/url]", pwszLocalName);
}
else m_bOfflineDownloaded = uint8_t(100.0 * blob.getTransferred() / blob.getSize());
buf.Append(blob.getName() ? blob.getName() : TranslateT("Unnamed"));
if (auto *pwszDescr = blob.getDescr()) {
buf.Append(L" - ");
buf.Append(pwszDescr);
}
if (uint32_t size = blob.getSize())
buf.AppendFormat(TranslateT(" %u KB"), size < 1024 ? 1 : unsigned(blob.getSize() / 1024));
wtext = buf.Detach();
pOwner->MarkRead(this);
break;
}
wchar_t buf[MAX_PATH];
File::GetReceivedFolder(hContact, buf, _countof(buf));
CMStringW wszFileName = buf;
wszFileName.Append(blob.getName());
wszFileName.Replace('\\', '/');
wszFileName.Insert(0, L"[url]file://");
wszFileName.Append(L"[/url]");
wtext = wszFileName.Detach();
}
break;
default:
wtext = DbEvent_GetTextW(&dbe);
break;
}
if (dbe.szReplyId)
if (MEVENT hReply = db_event_getById(dbe.szModule, dbe.szReplyId)) {
DB::EventInfo dbei(hReply);
if (dbei) {
CMStringW str, wszNick;
wchar_t wszTime[100];
TimeZone_PrintTimeStamp(0, dbe.timestamp, L"D t", wszTime, _countof(wszTime), 0);
if (Contact::IsGroupChat(hContact) && dbe.szUserId)
wszNick = Utf2T(dbe.szUserId);
else if (dbei.flags & DBEF_SENT) {
if (char *szProto = Proto_GetBaseAccountName(hContact))
wszNick = ptrW(Contact::GetInfo(CNF_DISPLAY, 0, szProto));
else
wszNick = TranslateT("I"); // shall never happen
}
else wszNick = Clist_GetContactDisplayName(hContact, 0);
str.AppendFormat(L"%s %s %s:\n", wszTime, wszNick.c_str(), TranslateT("wrote"));
ptrW wszText(DbEvent_GetTextW(&dbei));
if (mir_wstrlen(wszText) > 43)
wcscpy(wszText.get() + 40, L"...");
str.Append(wszText);
qtext = str.Detach();
}
}
dbe.unload();
}
void ItemData::setText(const wchar_t *pwszText)
{
savedHeight = -1;
int fontid, colorid;
getFontColor(fontid, colorid);
pOwner->webPage.clText = g_fontTable[fontid].cl;
pOwner->webPage.clBack = g_colorTable[colorid].cl;
m_doc = litehtml::document::createFromString(T2Utf(formatHtml(pwszText)), &pOwner->webPage);
}
// Array
HistoryArray::HistoryArray() :
pages(50),
strings(50, wcscmp)
{
}
HistoryArray::~HistoryArray()
{
clear();
}
void HistoryArray::clear()
{
for (auto &str : strings)
mir_free(str);
strings.destroy();
pages.destroy();
iLastPageCounter = 0;
}
void HistoryArray::addChatEvent(NewstoryListData *pOwner, SESSION_INFO *si, const LOGINFO *lin)
{
int numItems = getCount();
auto &p = allocateItem();
p.pOwner = pOwner;
p.hContact = si->hContact;
if (si->pMI->bDatabase && lin->hEvent) {
p.dbe = lin->hEvent;
checkGC(p, si);
}
else {
CMStringW wszText;
bool bTextUsed = Chat_GetDefaultEventDescr(si, lin, wszText);
if (!bTextUsed && lin->ptszText) {
if (!wszText.IsEmpty())
wszText.AppendChar(' ');
wszText.Append(g_chatApi.RemoveFormatting(lin->ptszText));
}
p.wtext = wszText.Detach();
p.m_bLoaded = true;
p.m_bHighlighted = lin->bIsHighlighted;
p.dbe.timestamp = lin->time;
if (lin->bIsMe)
p.dbe.flags |= DBEF_SENT;
switch (lin->iType) {
case GC_EVENT_MESSAGE:
case GC_EVENT_INFORMATION:
p.dbe.eventType = EVENTTYPE_MESSAGE;
break;
case GC_EVENT_SETCONTACTSTATUS:
p.dbe.eventType = EVENTTYPE_STATUSCHANGE;
break;
case GC_EVENT_JOIN:
case GC_EVENT_PART:
case GC_EVENT_QUIT:
p.dbe.eventType = EVENTTYPE_JABBER_PRESENCE;
break;
default:
p.dbe.eventType = EVENTTYPE_OTHER;
break;
}
}
if (lin->ptszNick) {
addNick(p, lin->ptszNick);
p.checkPrevGC((numItems == 0) ? nullptr : get(numItems - 1));
}
}
bool HistoryArray::addEvent(NewstoryListData *pOwner, MCONTACT hContact, MEVENT hEvent, int count)
{
if (count == -1)
count = MAXINT;
int numItems = getCount();
auto *pPrev = (numItems == 0) ? nullptr : get(numItems - 1);
SESSION_INFO *si = nullptr;
if (Contact::IsGroupChat(hContact))
si = Chat_Find(hContact);
if (count == 1) {
auto &p = allocateItem();
p.pOwner = pOwner;
p.hContact = hContact;
p.dbe = hEvent;
if (si) {
checkGC(p, si);
pPrev = p.checkPrevGC(pPrev);
}
else pPrev = p.checkPrev(pPrev);
}
else {
DB::ECPTR pCursor(DB::Events(hContact, hEvent));
for (int i = 0; i < count; i++) {
hEvent = pCursor.FetchNext();
if (!hEvent)
break;
auto &p = allocateItem();
p.pOwner = pOwner;
p.hContact = hContact;
p.dbe = hEvent;
if (si) {
checkGC(p, si);
pPrev = p.checkPrevGC(pPrev);
}
else pPrev = p.checkPrev(pPrev);
}
}
return true;
}
void HistoryArray::addNick(ItemData &pItem, wchar_t *pwszNick)
{
pItem.wszNick = strings.find(pwszNick);
if (pItem.wszNick == nullptr) {
pItem.wszNick = mir_wstrdup(pwszNick);
strings.insert(pItem.wszNick);
}
}
void HistoryArray::addResults(NewstoryListData *pOwner, const OBJLIST &pArray)
{
int numItems = getCount();
auto *pPrev = (numItems == 0) ? nullptr : get(numItems - 1);
for (auto &it : pArray) {
auto &p = allocateItem();
p.pOwner = pOwner;
p.hContact = it->hContact;
p.dbe = it->hEvent;
p.m_bIsResult = true;
pPrev = p.checkPrev(pPrev);
}
}
ItemData& HistoryArray::allocateItem()
{
if (iLastPageCounter == HIST_BLOCK_SIZE) {
pages.insert(new ItemBlock());
iLastPageCounter = 0;
}
else if (pages.getCount() == 0)
pages.insert(new ItemBlock);
auto &p = pages[pages.getCount() - 1];
return p.data[iLastPageCounter++];
}
void HistoryArray::checkGC(ItemData &p, SESSION_INFO *si)
{
p.load();
if (p.dbe.szUserId) {
Utf2T wszUser(p.dbe.szUserId);
if (auto *pUser = g_chatApi.UM_FindUser(si, wszUser))
addNick(p, pUser->pszNick);
else
addNick(p, wszUser);
}
}
int HistoryArray::find(MEVENT hEvent)
{
int i = 0;
for (auto &it : pages)
for (auto &p : it->data) {
if (p.dbe.getEvent() == hEvent)
return i;
i++;
}
return -1;
}
ItemData* HistoryArray::get(int id, bool bLoad) const
{
int pageNo = id / HIST_BLOCK_SIZE;
if (pageNo >= pages.getCount())
return nullptr;
auto *p = &pages[pageNo].data[id % HIST_BLOCK_SIZE];
if (bLoad && !p->m_bLoaded)
p->load();
return p;
}
int HistoryArray::getCount() const
{
int nPages = pages.getCount();
return (nPages == 0) ? 0 : (nPages - 1) * HIST_BLOCK_SIZE + iLastPageCounter;
}
int HistoryArray::find(int id, int dir, const Filter &filter)
{
int count = getCount();
for (int i = id + dir; i >= 0 && i < count; i += dir)
if (filter.check(get(i)))
return i;
return -1;
}
ItemData* HistoryArray::insert(int pos)
{
int count = getCount();
ItemData *pNew = &allocateItem();
if (count > 0) {
ItemData *pPrev = get(count - 1, false);
for (int i = count; i >= pos; i--) {
memcpy(pNew, pPrev, sizeof(ItemData));
pNew = pPrev;
pPrev = get(i - 1, false);
}
}
ItemData tmp;
memcpy(pNew, &tmp, sizeof(tmp));
return pNew;
}
void HistoryArray::remove(int id)
{
int pageNo = id / HIST_BLOCK_SIZE;
if (pageNo >= pages.getCount())
return;
auto &pPage = pages[pageNo];
int offset = id % HIST_BLOCK_SIZE;
ItemData tmp;
memcpy(&tmp, pPage.data + offset, sizeof(ItemData));
if (offset != HIST_BLOCK_SIZE - 1)
memmove(&pPage.data[offset], &pPage.data[offset+1], sizeof(ItemData) * (HIST_BLOCK_SIZE - 1 - offset));
for (int i = pageNo + 1; i < pages.getCount(); i++) {
auto &prev = pages[i - 1], &curr = pages[i];
memcpy(&prev.data[HIST_BLOCK_SIZE - 1], curr.data, sizeof(ItemData));
memmove(&curr.data, &curr.data[1], sizeof(ItemData) * (HIST_BLOCK_SIZE - 1));
memset(&curr.data[HIST_BLOCK_SIZE - 1], 0, sizeof(ItemData));
}
if (iLastPageCounter == 1) {
pages.remove(pages.getCount() - 1);
iLastPageCounter = HIST_BLOCK_SIZE;
}
else iLastPageCounter--;
}