Bug 787149 - Change Chat log tree so that it aggregates logs into days. r=florian, ui-r=bwinton.
This commit is contained in:
Родитель
20e1e897df
Коммит
6c2cb157b4
|
@ -42,14 +42,16 @@ interface imILog: nsISupports {
|
|||
imILogConversation getConversation();
|
||||
};
|
||||
|
||||
[scriptable, uuid(ab38c01c-2245-4279-9437-1d6bcc69d556)]
|
||||
[scriptable, uuid(327ba58c-ee9c-4d1c-9216-fd505c45a3e0)]
|
||||
interface imILogger: nsISupports {
|
||||
imILog getLogFromFile(in nsIFile aFile);
|
||||
imILog getLogFromFile(in nsIFile aFile, [optional] in boolean aGroupByDay);
|
||||
nsIFile getLogFileForOngoingConversation(in prplIConversation aConversation);
|
||||
nsISimpleEnumerator getLogsForAccountBuddy(in imIAccountBuddy aAccountBuddy);
|
||||
nsISimpleEnumerator getLogsForBuddy(in imIBuddy aBuddy);
|
||||
nsISimpleEnumerator getLogsForContact(in imIContact aContact);
|
||||
nsISimpleEnumerator getLogsForConversation(in prplIConversation aConversation);
|
||||
nsISimpleEnumerator getLogsForConversation(in prplIConversation aConversation,
|
||||
[optional] in boolean aGroupByDay);
|
||||
nsISimpleEnumerator getSystemLogsForAccount(in imIAccount aAccount);
|
||||
nsISimpleEnumerator getSimilarLogs(in imILog aLog);
|
||||
nsISimpleEnumerator getSimilarLogs(in imILog aLog,
|
||||
[optional] in boolean aGroupByDay);
|
||||
};
|
||||
|
|
|
@ -74,7 +74,7 @@ ConversationLog.prototype = {
|
|||
_init: function cl_init() {
|
||||
let file = getLogFolderForAccount(this._conv.account, true);
|
||||
let name = this._conv.normalizedName;
|
||||
if (this._conv.isChat && this._conv.account.protocol.id != "prpl-twitter")
|
||||
if (convIsRealMUC(this._conv))
|
||||
name += ".chat";
|
||||
file.append(name);
|
||||
if (!file.exists())
|
||||
|
@ -299,31 +299,46 @@ function LogMessage(aData, aConversation)
|
|||
}
|
||||
LogMessage.prototype = GenericMessagePrototype;
|
||||
|
||||
function LogConversation(aLineInputStream)
|
||||
function LogConversation(aLineInputStreams)
|
||||
{
|
||||
let line = {value: ""};
|
||||
let more = aLineInputStream.readLine(line);
|
||||
|
||||
if (!line.value)
|
||||
throw "bad log file";
|
||||
|
||||
let data = JSON.parse(line.value);
|
||||
this.name = data.name;
|
||||
this.title = data.title;
|
||||
this._accountName = data.account;
|
||||
this._protocolName = data.protocol;
|
||||
// If aLineInputStreams isn't an Array, we'll assume that it's a lone
|
||||
// InputStream, and wrap it in an Array.
|
||||
if (!Array.isArray(aLineInputStreams))
|
||||
aLineInputStreams = [aLineInputStreams];
|
||||
|
||||
this._messages = [];
|
||||
while (more) {
|
||||
more = aLineInputStream.readLine(line);
|
||||
|
||||
// We'll read the name, title, account, and protocol data from the first
|
||||
// stream, and skip the others.
|
||||
let firstFile = true;
|
||||
|
||||
for each (let inputStream in aLineInputStreams) {
|
||||
let line = {value: ""};
|
||||
let more = inputStream.readLine(line);
|
||||
|
||||
if (!line.value)
|
||||
break;
|
||||
try {
|
||||
throw "bad log file";
|
||||
|
||||
if (firstFile) {
|
||||
let data = JSON.parse(line.value);
|
||||
this._messages.push(new LogMessage(data, this));
|
||||
} catch (e) {
|
||||
// if a message line contains junk, just ignore the error and
|
||||
// continue reading the conversation.
|
||||
this.name = data.name;
|
||||
this.title = data.title;
|
||||
this._accountName = data.account;
|
||||
this._protocolName = data.protocol;
|
||||
firstFile = false;
|
||||
}
|
||||
|
||||
while (more) {
|
||||
more = inputStream.readLine(line);
|
||||
if (!line.value)
|
||||
break;
|
||||
try {
|
||||
let data = JSON.parse(line.value);
|
||||
this._messages.push(new LogMessage(data, this));
|
||||
} catch (e) {
|
||||
// if a message line contains junk, just ignore the error and
|
||||
// continue reading the conversation.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -350,19 +365,15 @@ function Log(aFile)
|
|||
{
|
||||
this.file = aFile;
|
||||
this.path = aFile.path;
|
||||
const regexp = /([0-9]{4})-([0-9]{2})-([0-9]{2}).([0-9]{2})([0-9]{2})([0-9]{2})([+-])([0-9]{2})([0-9]{2}).*\.([a-z]+)$/;
|
||||
let r = aFile.leafName.match(regexp);
|
||||
if (!r) {
|
||||
|
||||
let [date, format] = getDateFromFilename(aFile.leafName);
|
||||
if (!date || !format) {
|
||||
this.format = "invalid";
|
||||
this.time = 0;
|
||||
return;
|
||||
}
|
||||
let date = new Date(r[1], r[2] - 1, r[3], r[4], r[5], r[6]);
|
||||
let offset = r[7] * 60 + r[8];
|
||||
if (r[6] == -1)
|
||||
offset *= -1;
|
||||
this.time = date.valueOf() / 1000; // ignore the timezone offset for now (FIXME)
|
||||
this.format = r[10];
|
||||
this.time = date.valueOf() / 1000;
|
||||
this.format = format;
|
||||
}
|
||||
Log.prototype = {
|
||||
__proto__: ClassInfo("imILog", "Log object"),
|
||||
|
@ -388,6 +399,38 @@ Log.prototype = {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a properly formatted log file name and extracts the date information
|
||||
* and filetype, returning the results as an Array.
|
||||
*
|
||||
* Filenames are expected to be formatted as:
|
||||
*
|
||||
* YYYY-MM-DD.HHmmSS+ZZzz.format
|
||||
*
|
||||
* @param aFilename the name of the file
|
||||
* @returns an Array, where the first element is a Date object for the date
|
||||
* that the log file represents, and the file type as a string.
|
||||
*/
|
||||
function getDateFromFilename(aFilename) {
|
||||
const kRegExp = /([\d]{4})-([\d]{2})-([\d]{2}).([\d]{2})([\d]{2})([\d]{2})([+-])([\d]{2})([\d]{2}).*\.([A-Za-z]+)$/;
|
||||
|
||||
let r = aFilename.match(kRegExp);
|
||||
if (!r)
|
||||
return [];
|
||||
|
||||
// We ignore the timezone offset for now (FIXME)
|
||||
return [new Date(r[1], r[2] - 1, r[3], r[4], r[5], r[6]), r[10]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a Conversation is both a chat conversation, and not
|
||||
* a Twitter conversation.
|
||||
*/
|
||||
function convIsRealMUC(aConversation) {
|
||||
return (aConversation.isChat &&
|
||||
aConversation.account.protocol.id != "prpl-twitter");
|
||||
}
|
||||
|
||||
function LogEnumerator(aEntries)
|
||||
{
|
||||
this._entries = aEntries;
|
||||
|
@ -404,17 +447,158 @@ LogEnumerator.prototype = {
|
|||
QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator])
|
||||
};
|
||||
|
||||
function DailyLogEnumerator(aEntries) {
|
||||
this._entries = {};
|
||||
|
||||
for each (entry in aEntries) {
|
||||
while (entry.hasMoreElements()) {
|
||||
let file = entry.getNext();
|
||||
if (!(file instanceof Ci.nsIFile))
|
||||
continue;
|
||||
|
||||
let [logDate] = getDateFromFilename(file.leafName);
|
||||
if (!logDate) {
|
||||
// We'll skip this one, since it's got a busted filename.
|
||||
continue;
|
||||
}
|
||||
|
||||
// We want to cluster all of the logs that occur on the same day
|
||||
// into the same Arrays. We clone the date for the log, reset it to
|
||||
// the 0th hour/minute/second, and use that to construct an ID for the
|
||||
// Array we'll put the log in.
|
||||
let dateForID = new Date(logDate);
|
||||
dateForID.setHours(0);
|
||||
dateForID.setMinutes(0);
|
||||
dateForID.setSeconds(0);
|
||||
let dayID = dateForID.toISOString();
|
||||
|
||||
if (!(dayID in this._entries))
|
||||
this._entries[dayID] = [];
|
||||
|
||||
this._entries[dayID].push({
|
||||
file: file,
|
||||
time: logDate
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._days = Object.keys(this._entries).sort();
|
||||
this._index = 0;
|
||||
}
|
||||
DailyLogEnumerator.prototype = {
|
||||
_entries: {},
|
||||
_days: [],
|
||||
_index: 0,
|
||||
hasMoreElements: function() this._index < this._days.length,
|
||||
getNext: function() new LogCluster(this._entries[this._days[this._index++]]),
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator])
|
||||
};
|
||||
|
||||
/**
|
||||
* A LogCluster is a Log representing several log files all at once. The
|
||||
* constructor expects aEntries, which is an array of objects that each
|
||||
* have two properties: file and time. The file is the nsIFile for the
|
||||
* log file, and the time is the Date object extracted from the filename for
|
||||
* the log file.
|
||||
*/
|
||||
function LogCluster(aEntries) {
|
||||
if (!aEntries.length)
|
||||
throw new Error("LogCluster was passed an empty Array");
|
||||
|
||||
// Sort our list of entries for this day in increasing order.
|
||||
aEntries.sort(function(aLeft, aRight) aLeft.time - aRight.time);
|
||||
|
||||
this._entries = aEntries;
|
||||
// Calculate the timestamp for the first entry down to the day.
|
||||
let timestamp = new Date(aEntries[0].time);
|
||||
timestamp.setHours(0);
|
||||
timestamp.setMinutes(0);
|
||||
timestamp.setSeconds(0);
|
||||
this.time = timestamp.valueOf() / 1000;
|
||||
// Path is used to uniquely identify a Log, and sometimes used to
|
||||
// quickly determine which directory a log file is from. We'll use
|
||||
// the first file's path.
|
||||
this.path = aEntries[0].file.path;
|
||||
}
|
||||
LogCluster.prototype = {
|
||||
__proto__: ClassInfo("imILog", "LogCluster object"),
|
||||
format: "json",
|
||||
|
||||
getConversation: function() {
|
||||
const PR_RDONLY = 0x01;
|
||||
let streams = [];
|
||||
for each (let entry in this._entries) {
|
||||
let fis = new FileInputStream(entry.file, PR_RDONLY, 0444,
|
||||
Ci.nsIFileInputStream.CLOSE_ON_EOF);
|
||||
// Pass in 0x0 so that we throw exceptions on unknown bytes.
|
||||
let lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
|
||||
lis.QueryInterface(Ci.nsIUnicharLineInputStream);
|
||||
streams.push(lis);
|
||||
}
|
||||
|
||||
try {
|
||||
return new LogConversation(streams);
|
||||
} catch (e) {
|
||||
// If the file contains some junk (invalid JSON), the
|
||||
// LogConversation code will still read the messages it can parse.
|
||||
// If the first line of meta data is corrupt, there's really no
|
||||
// useful data we can extract from the file so the
|
||||
// LogConversation constructor will throw.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function Logger() { }
|
||||
Logger.prototype = {
|
||||
_enumerateLogs: function logger__enumerateLogs(aAccount, aNormalizedName) {
|
||||
_enumerateLogs: function logger__enumerateLogs(aAccount, aNormalizedName,
|
||||
aGroupByDay) {
|
||||
let file = getLogFolderForAccount(aAccount);
|
||||
file.append(aNormalizedName);
|
||||
if (!file.exists())
|
||||
return EmptyEnumerator;
|
||||
|
||||
return new LogEnumerator([file.directoryEntries]);
|
||||
let enumerator = aGroupByDay ? DailyLogEnumerator : LogEnumerator;
|
||||
|
||||
return new enumerator([file.directoryEntries]);
|
||||
},
|
||||
getLogFromFile: function logger_getLogFromFile(aFile, aGroupByDay) {
|
||||
if (aGroupByDay)
|
||||
return this._getDailyLogFromFile(aFile);
|
||||
|
||||
return new Log(aFile);
|
||||
},
|
||||
_getDailyLogFromFile: function logger_getDailyLogsForFile(aFile) {
|
||||
let [targetDate] = getDateFromFilename(aFile.leafName);
|
||||
if (!targetDate)
|
||||
return null;
|
||||
|
||||
let targetDay = Math.floor(targetDate / (86400 * 1000));
|
||||
|
||||
// Get the path for the log file - we'll assume that the files relevant
|
||||
// to our interests are in the same folder.
|
||||
let path = aFile.path;
|
||||
let folder = aFile.parent.directoryEntries;
|
||||
let relevantEntries = [];
|
||||
// Pick out the files that start within our date range.
|
||||
while (folder.hasMoreElements()) {
|
||||
let file = folder.getNext();
|
||||
if (!(file instanceof Ci.nsIFile))
|
||||
continue;
|
||||
|
||||
let [logTime] = getDateFromFilename(file.leafName);
|
||||
|
||||
let day = Math.floor(logTime / (86400 * 1000));
|
||||
if (targetDay == day) {
|
||||
relevantEntries.push({
|
||||
file: file,
|
||||
time: logTime
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new LogCluster(relevantEntries);
|
||||
},
|
||||
getLogFromFile: function logger_getLogFromFile(aFile) new Log(aFile),
|
||||
getLogFileForOngoingConversation: function logger_getLogFileForOngoingConversation(aConversation)
|
||||
getLogForConversation(aConversation).file,
|
||||
getLogsForContact: function logger_getLogsForContact(aContact) {
|
||||
|
@ -441,17 +625,20 @@ Logger.prototype = {
|
|||
},
|
||||
getLogsForAccountBuddy: function logger_getLogsForAccountBuddy(aAccountBuddy)
|
||||
this._enumerateLogs(aAccountBuddy.account, aAccountBuddy.normalizedName),
|
||||
getLogsForConversation: function logger_getLogsForConversation(aConversation) {
|
||||
getLogsForConversation: function logger_getLogsForConversation(aConversation,
|
||||
aGroupByDay) {
|
||||
let name = aConversation.normalizedName;
|
||||
if (aConversation.isChat &&
|
||||
aConversation.account.protocol.id != "prpl-twitter")
|
||||
if (convIsRealMUC(aConversation))
|
||||
name += ".chat";
|
||||
return this._enumerateLogs(aConversation.account, name);
|
||||
|
||||
return this._enumerateLogs(aConversation.account, name, aGroupByDay);
|
||||
},
|
||||
getSystemLogsForAccount: function logger_getSystemLogsForAccount(aAccount)
|
||||
this._enumerateLogs(aAccount, ".system"),
|
||||
getSimilarLogs: function(aLog)
|
||||
new LogEnumerator([new LocalFile(aLog.path).parent.directoryEntries]),
|
||||
getSimilarLogs: function(aLog, aGroupByDay) {
|
||||
let enumerator = aGroupByDay ? DailyLogEnumerator : LogEnumerator;
|
||||
return new enumerator([new LocalFile(aLog.path).parent.directoryEntries]);
|
||||
},
|
||||
|
||||
observe: function logger_observe(aSubject, aTopic, aData) {
|
||||
switch (aTopic) {
|
||||
|
|
|
@ -548,7 +548,7 @@ var chatHandler = {
|
|||
if (item.getAttribute("id") == "searchResultConv") {
|
||||
let path = "logs/" + item.log.path;
|
||||
let file = FileUtils.getFile("ProfD", path.split("/"));
|
||||
let log = imServices.logs.getLogFromFile(file);
|
||||
let log = imServices.logs.getLogFromFile(file, true);
|
||||
document.getElementById("goToConversation").hidden = true;
|
||||
document.getElementById("contextPane").removeAttribute("chat");
|
||||
let conv = log.getConversation();
|
||||
|
@ -563,8 +563,7 @@ var chatHandler = {
|
|||
cti.removeAttribute("statusTooltiptext");
|
||||
cti.removeAttribute("topicEditable");
|
||||
cti.removeAttribute("noTopic");
|
||||
|
||||
this._showLogList(imServices.logs.getSimilarLogs(log), log);
|
||||
this._showLogList(imServices.logs.getSimilarLogs(log, true), log);
|
||||
this.observedContact = null;
|
||||
}
|
||||
else if (item.localName == "imconv") {
|
||||
|
@ -587,11 +586,11 @@ var chatHandler = {
|
|||
item.convView.updateConvStatus();
|
||||
item.update();
|
||||
|
||||
this._showLogList(imServices.logs.getLogsForConversation(item.conv));
|
||||
this._showLogList(imServices.logs.getLogsForConversation(item.conv, true));
|
||||
let contextPane = document.getElementById("contextPane");
|
||||
if (item.conv.isChat) {
|
||||
contextPane.setAttribute("chat", "true");
|
||||
item.convView.showParticipants();
|
||||
item.convView.showParticipants();
|
||||
}
|
||||
else
|
||||
contextPane.removeAttribute("chat");
|
||||
|
@ -1053,15 +1052,16 @@ chatLogTreeGroupItem.prototype = {
|
|||
getProperties: function(aProps) {}
|
||||
};
|
||||
|
||||
function chatLogTreeLogItem(aLog, aText) {
|
||||
function chatLogTreeLogItem(aLog, aText, aLevel) {
|
||||
this.log = aLog;
|
||||
this._text = aText;
|
||||
this._level = aLevel;
|
||||
}
|
||||
chatLogTreeLogItem.prototype = {
|
||||
getText: function() this._text,
|
||||
get id() this.log.title,
|
||||
get open() false,
|
||||
get level() 1,
|
||||
get level() this._level,
|
||||
get children() [],
|
||||
getProperties: function(aProps) {}
|
||||
};
|
||||
|
@ -1076,6 +1076,11 @@ chatLogTreeView.prototype = {
|
|||
__proto__: new PROTO_TREE_VIEW(),
|
||||
|
||||
_rebuild: function cLTV__rebuild() {
|
||||
// Some date helpers...
|
||||
const kDayInMsecs = 24 * 60 * 60 * 1000;
|
||||
const kWeekInMsecs = 7 * kDayInMsecs;
|
||||
const kTwoWeeksInMsecs = 2 * kWeekInMsecs;
|
||||
|
||||
// Drop the old rowMap.
|
||||
if (this._tree)
|
||||
this._tree.rowCountChanged(0, -this._rowMap.length);
|
||||
|
@ -1084,17 +1089,15 @@ chatLogTreeView.prototype = {
|
|||
// The keys used in the 'groups' object should match string ids in
|
||||
// messenger.properties, except 'other' that has a special handling.
|
||||
let groups = {
|
||||
today: [],
|
||||
yesterday: [],
|
||||
lastWeek: [],
|
||||
twoWeeksAgo: [],
|
||||
other: []
|
||||
};
|
||||
|
||||
// Some date helpers...
|
||||
const kDayInMsecs = 24 * 60 * 60 * 1000;
|
||||
const kWeekInMsecs = 7 * kDayInMsecs;
|
||||
const kTwoWeeksInMsecs = 2 * kWeekInMsecs;
|
||||
// today and yesterday are treated differently, because they represent
|
||||
// individual logs, and are not "groups".
|
||||
let today = null, yesterday = null;
|
||||
|
||||
let dts = Components.classes["@mozilla.org/intl/scriptabledateformat;1"]
|
||||
.getService(Ci.nsIScriptableDateFormat);
|
||||
let formatDate = function(aDate) {
|
||||
|
@ -1106,38 +1109,46 @@ chatLogTreeView.prototype = {
|
|||
nowDate.getDate());
|
||||
|
||||
// Build a chatLogTreeLogItem for each log, and put it in the right group.
|
||||
let chatBundle = document.getElementById("chatBundle");
|
||||
let msgBundle = document.getElementById("bundle_messenger");
|
||||
|
||||
for each (let log in fixIterator(this._logs)) {
|
||||
let logDate = new Date(log.time * 1000);
|
||||
let timeFromToday = todayDate - logDate;
|
||||
let title = dts.FormatTime("", dts.timeFormatNoSeconds,
|
||||
logDate.getHours(), logDate.getMinutes(), 0);
|
||||
if (timeFromToday > kDayInMsecs) {
|
||||
title = chatBundle.getFormattedString("dateTime",
|
||||
[formatDate(logDate), title]);
|
||||
}
|
||||
let title = formatDate(logDate);
|
||||
let group;
|
||||
if (timeFromToday <= 0)
|
||||
group = groups.today;
|
||||
else if (timeFromToday <= kDayInMsecs)
|
||||
group = groups.yesterday;
|
||||
if (timeFromToday <= 0) {
|
||||
today = new chatLogTreeLogItem(log, msgBundle.getString("today"), 0);
|
||||
continue;
|
||||
}
|
||||
else if (timeFromToday <= kDayInMsecs) {
|
||||
yesterday = new chatLogTreeLogItem(log, msgBundle.getString("yesterday"), 0);
|
||||
continue;
|
||||
}
|
||||
else if (timeFromToday <= kWeekInMsecs)
|
||||
group = groups.lastWeek;
|
||||
else if (timeFromToday <= kTwoWeeksInMsecs)
|
||||
group = groups.twoWeeksAgo;
|
||||
else
|
||||
group = groups.other;
|
||||
group.push(new chatLogTreeLogItem(log, title));
|
||||
group.push(new chatLogTreeLogItem(log, title, 1));
|
||||
}
|
||||
|
||||
// Create a chatLogTreeGroupItem for each group.
|
||||
let msgBundle = document.getElementById("bundle_messenger");
|
||||
if (today)
|
||||
this._rowMap.push(today);
|
||||
if (yesterday)
|
||||
this._rowMap.push(yesterday);
|
||||
|
||||
for each (let [groupId, group] in Iterator(groups)) {
|
||||
if (!group.length)
|
||||
continue;
|
||||
|
||||
group.sort(function(l1, l2) l2.log.time - l1.log.time);
|
||||
|
||||
let groupName;
|
||||
if (groupId == "other") {
|
||||
// If we're in the "other" group, the title will be the end and
|
||||
// beginning dates for that group.
|
||||
// Example: 28/08/2012 - 04/01/2012
|
||||
groupName = formatDate(new Date(group[0].log.time * 1000));
|
||||
if (group.length > 1) {
|
||||
let fromDate = new Date(group[group.length - 1].log.time * 1000);
|
||||
|
@ -1145,6 +1156,7 @@ chatLogTreeView.prototype = {
|
|||
}
|
||||
}
|
||||
else {
|
||||
// Otherwise, get the appropriate string for this group.
|
||||
groupName = msgBundle.getString(groupId);
|
||||
}
|
||||
this._rowMap.push(new chatLogTreeGroupItem(groupName, group));
|
||||
|
|
Загрузка…
Ссылка в новой задаче