Display relative timestamps in conversation list
This mimicks Signal-Android's relative timestamps. Previously, only the date was displayed. Fixes #284
This commit is contained in:
parent
05f4b559fd
commit
e876d8f6ed
6 changed files with 142 additions and 18 deletions
|
@ -143,7 +143,7 @@
|
|||
<script type='text/x-tmpl-mustache' id='conversation-preview'>
|
||||
{{> avatar }}
|
||||
<div class='contact-details'>
|
||||
<span class='last-timestamp'> {{ last_message_timestamp }} </span>
|
||||
<span class='last-timestamp' data-timestamp={{ last_message_timestamp }}> </span>
|
||||
{{> contact_name_and_number }}
|
||||
{{ #unreadCount }}
|
||||
<span class='unread-count'>{{ unreadCount }}</span>
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
this.listenTo(this.model, 'destroy', this.remove); // auto update
|
||||
this.listenTo(this.model, 'opened', this.markSelected); // auto update
|
||||
extension.windows.onClosed(this.stopListening.bind(this));
|
||||
this.timeStampView = new Whisper.BriefTimestampView();
|
||||
},
|
||||
|
||||
markSelected: function() {
|
||||
|
@ -36,12 +37,14 @@
|
|||
Mustache.render(_.result(this,'template', ''), {
|
||||
title: this.model.getTitle(),
|
||||
last_message: this.model.get('lastMessage'),
|
||||
last_message_timestamp: moment(this.model.get('timestamp')).format('MMM D'),
|
||||
last_message_timestamp: this.model.get('timestamp'),
|
||||
number: this.model.getNumber(),
|
||||
avatar: this.model.getAvatar(),
|
||||
unreadCount: this.model.get('unreadCount')
|
||||
}, this.render_partials())
|
||||
);
|
||||
this.timeStampView.setElement(this.$('.last-timestamp'));
|
||||
this.timeStampView.update();
|
||||
|
||||
twemoji.parse(this.el, { base: '/images/twemoji/', size: 16 });
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
this.listenTo(this.model, 'destroy', this.remove);
|
||||
this.listenTo(this.model, 'pending', this.renderPending);
|
||||
this.listenTo(this.model, 'done', this.renderDone);
|
||||
this.timeStampView = new Whisper.MessageTimestampView();
|
||||
this.timeStampView = new Whisper.ExtendedTimestampView();
|
||||
},
|
||||
events: {
|
||||
'click .timestamp': 'select',
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.MessageTimestampView = Whisper.View.extend({
|
||||
Whisper.TimestampView = Whisper.View.extend({
|
||||
initialize: function() {
|
||||
extension.windows.onClosed(this.clearTimeout.bind(this));
|
||||
},
|
||||
|
@ -16,19 +16,8 @@
|
|||
if (millis >= millis_now) {
|
||||
millis = millis_now;
|
||||
}
|
||||
var lastWeek = moment(millis_now).subtract(7, 'days');
|
||||
var time = moment(millis);
|
||||
var result = '';
|
||||
if (time > moment(millis_now).startOf('day')) {
|
||||
// t units ago
|
||||
result = time.fromNow();
|
||||
} else if (time > lastWeek) {
|
||||
// Fri 1:30 PM or Fri 13:30
|
||||
result = time.format('ddd ') + time.format('LT');
|
||||
} else {
|
||||
// Oct 31 1:30 PM or Oct 31
|
||||
result = time.format('MMM D ') + time.format('LT');
|
||||
}
|
||||
// defined in subclass!
|
||||
var result = this.getRelativeTimeSpanString(millis);
|
||||
this.$el.text(result);
|
||||
|
||||
var delay;
|
||||
|
@ -42,7 +31,7 @@
|
|||
} else if (millis_since <= moment.relativeTimeThreshold('h') * 1000 * 60 * 60) {
|
||||
// N hours ago
|
||||
delay = 60 * 60 * 1000;
|
||||
} else if (time > lastWeek) {
|
||||
} else { // more than a week ago
|
||||
// Day of week + time
|
||||
delay = 7 * 24 * 60 * 60 * 1000 - millis_since;
|
||||
}
|
||||
|
@ -56,4 +45,63 @@
|
|||
clearTimeout(this.timeout);
|
||||
}
|
||||
});
|
||||
|
||||
Whisper.BriefTimestampView = Whisper.TimestampView.extend({
|
||||
getRelativeTimeSpanString: function(timestamp_) {
|
||||
// Convert to moment timestamp if it isn't already
|
||||
var timestamp = moment(timestamp_),
|
||||
timediff = moment.duration(moment() - timestamp);
|
||||
|
||||
// Do some wrapping to match conversation view, display >= 30 minutes (seconds)
|
||||
// as a full hour (minute) if the number of hours (minutes) isn't zero
|
||||
if (timediff.hours >= 1 && timediff.minutes() >= 30) {
|
||||
timediff.add(1, 'hours');
|
||||
} else if (timediff.minutes() >= 1 && timediff.seconds() >= 30) {
|
||||
timediff.add(1, 'minutes');
|
||||
}
|
||||
|
||||
if (timediff.years() > 0) {
|
||||
return timestamp.format('MMM D, YYYY');
|
||||
} else if (timediff.months() > 0 || timediff.days() > 6) {
|
||||
return timestamp.format('MMM D');
|
||||
} else if (timediff.days() > 0) {
|
||||
return timestamp.format('ddd');
|
||||
} else if (timediff.hours() > 1) {
|
||||
return timediff.hours() + ' hours';
|
||||
} else if (timediff.hours() === 1 || timediff.minutes() >= 45) {
|
||||
// to match conversation view, display >= 45 minutes as 1 hour
|
||||
return '1 hour';
|
||||
} else if (timediff.minutes() > 1) {
|
||||
return timediff.minutes() + ' min';
|
||||
} else if (timediff.minutes() === 1 || timediff.seconds() >= 45) {
|
||||
// same as hours/minutes, 0:00:45 -> 1 min
|
||||
return '1 min';
|
||||
} else {
|
||||
return 'now';
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Whisper.ExtendedTimestampView = Whisper.TimestampView.extend({
|
||||
getRelativeTimeSpanString: function(timestamp_) {
|
||||
var timestamp = moment(timestamp_),
|
||||
now = moment(),
|
||||
lastWeek = moment().subtract(7, 'days'),
|
||||
lastYear = moment().subtract(1, 'years');
|
||||
if (timestamp > now.startOf('day')) {
|
||||
// t units ago
|
||||
return timestamp.fromNow();
|
||||
} else if (timestamp > lastWeek) {
|
||||
// Fri 1:30 PM or Fri 13:30
|
||||
return timestamp.format('ddd ') + timestamp.format('LT');
|
||||
} else if (timestamp > lastYear) {
|
||||
// Oct 31 1:30 PM or Oct 31
|
||||
return timestamp.format('MMM D ') + timestamp.format('LT');
|
||||
} else {
|
||||
// Oct 31, 2012 1:30 PM
|
||||
return timestamp.format('MMM D, YYYY ') + timestamp.format('LT');
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -131,6 +131,7 @@
|
|||
<script type="text/javascript" src="views/whisper_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/group_update_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/message_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/timestamp_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/list_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/conversation_search_view_test.js"></script>
|
||||
<script type="text/javascript" src="models/conversations_test.js"></script>
|
||||
|
|
72
test/views/timestamp_view_test.js
Normal file
72
test/views/timestamp_view_test.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
describe('TimestampView', function() {
|
||||
it('formats long-ago timestamps correctly', function() {
|
||||
var timestamp = Date.now();
|
||||
var brief_view = new Whisper.BriefTimestampView().render(),
|
||||
ext_view = new Whisper.ExtendedTimestampView().render();
|
||||
|
||||
// Helper functions to check absolute and relative timestamps
|
||||
|
||||
// Helper to check an absolute TS for an exact match
|
||||
var check = function(view, ts, expected) {
|
||||
var result = view.getRelativeTimeSpanString(ts);
|
||||
assert.strictEqual(result, expected);
|
||||
};
|
||||
|
||||
// Helper to check relative times for an exact match against both views
|
||||
var checkDiff = function(sec_ago, expected_brief, expected_ext) {
|
||||
check(brief_view, timestamp - sec_ago * 1000, expected_brief);
|
||||
check(ext_view, timestamp - sec_ago * 1000, expected_ext);
|
||||
};
|
||||
|
||||
// Helper to check an absolute TS for an exact match against both views
|
||||
var checkAbs = function(ts, expected_brief, expected_ext) {
|
||||
if (!expected_ext) expected_ext = expected_brief;
|
||||
check(brief_view, ts, expected_brief);
|
||||
check(ext_view, ts, expected_ext);
|
||||
};
|
||||
|
||||
// Helper to check an absolute TS for a match at the beginning against
|
||||
var checkStartsWith = function(view, ts, expected) {
|
||||
var result = view.getRelativeTimeSpanString(ts);
|
||||
var regexp = new RegExp("^" + expected);
|
||||
assert.match(result, regexp);
|
||||
};
|
||||
|
||||
// check integer timestamp, JS Date object and moment object
|
||||
checkAbs(timestamp, 'now', 'a few seconds ago');
|
||||
checkAbs(new Date(), 'now', 'a few seconds ago');
|
||||
checkAbs(moment(), 'now', 'a few seconds ago');
|
||||
|
||||
// check recent timestamps
|
||||
checkDiff(30, 'now', 'a few seconds ago'); // 30 seconds
|
||||
checkDiff(50, '1 min', 'a minute ago'); // >= 45 seconds => 1 minute
|
||||
checkDiff(40*60, '40 min', '40 minutes ago');
|
||||
checkDiff(60*60, '1 hour', 'an hour ago');
|
||||
checkDiff(125*60, '2 hours', '2 hours ago');
|
||||
|
||||
// set to third of month to avoid problems on the 29th/30th/31st
|
||||
var last_month = moment().subtract(1, 'month').date(3),
|
||||
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
day_of_month = new Date().getDate();
|
||||
check(brief_view,last_month, months[last_month.month()] + ' 3');
|
||||
checkStartsWith(ext_view,last_month, months[last_month.month()] + ' 3');
|
||||
|
||||
// subtract 26 hours to be safe in case of DST stuff
|
||||
var yesterday = new Date(timestamp - 26*60*60*1000),
|
||||
days_of_week = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
check(brief_view, yesterday, days_of_week[yesterday.getDay()]);
|
||||
checkStartsWith(ext_view, yesterday, days_of_week[yesterday.getDay()]);
|
||||
|
||||
// Check something long ago
|
||||
// months are zero-indexed in JS for some reason
|
||||
check(brief_view, new Date(2012, 4, 5, 17, 30, 0), 'May 5, 2012');
|
||||
checkStartsWith(ext_view, new Date(2012, 4, 5, 17, 30, 0), 'May 5, 2012');
|
||||
});
|
||||
|
||||
});
|
Loading…
Reference in a new issue