Bug 787149 - Change Chat log tree so that it aggregates logs into days. r=florian, ui-r=bwinton.

This commit is contained in:
Mike Conley 2012-09-18 10:08:12 -04:00
Родитель 20e1e897df
Коммит 6c2cb157b4
3 изменённых файлов: 271 добавлений и 70 удалений

Просмотреть файл

@ -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));