commit
d21ab1f5e8
@ -1,26 +1,44 @@
|
|||||||
.btn span {
|
.btn {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border: 2px solid #7fd0ed;
|
border: 2px solid #acdbf5;
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.btn {
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
padding: 2px;
|
color: #7fd0ed;
|
||||||
border: none;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.btn:hover, .btn:focus {
|
.btn:hover, .btn:focus {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: none;
|
outline: none;
|
||||||
background-color: #f1fafd;
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #7fd0ed;
|
||||||
|
border-color: #acdbf5;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
.btn:active {
|
.btn:active {
|
||||||
outline: 2px dashed #acdbf5;
|
outline: 2px dashed #acdbf5;
|
||||||
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
.btn.selected span,
|
.btn.selected ,
|
||||||
.btn:active span {
|
.btn:active {
|
||||||
background-color: #7fd0ed;
|
background-color: #7fd0ed;
|
||||||
border: 2px solid #acdbf5;
|
border: 2px solid #acdbf5;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
.btn:active {
|
||||||
|
background-color: #f1fafd;
|
||||||
|
color: #7fd0ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-square {
|
||||||
|
display: inline-block;
|
||||||
|
width: 32px;
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.btn-square {
|
||||||
|
padding: 0;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
@ -0,0 +1,94 @@
|
|||||||
|
var Whisper = Whisper || {};
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Thread = Backbone.Model.extend({
|
||||||
|
defaults: function() {
|
||||||
|
return {
|
||||||
|
image: '/images/default.png',
|
||||||
|
unreadCount: 0,
|
||||||
|
timestamp: new Date().getTime()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
validate: function(attributes, options) {
|
||||||
|
var required = ['id', 'type', 'recipients', 'timestamp', 'image', 'name'];
|
||||||
|
var missing = _.filter(required, function(attr) { return !attributes[attr]; });
|
||||||
|
if (missing.length) { return "Thread must have " + missing; }
|
||||||
|
if (attributes.recipients.length === 0) {
|
||||||
|
return "No recipients for thread " + this.id;
|
||||||
|
}
|
||||||
|
for (var person in attributes.recipients) {
|
||||||
|
if (!person) return "Invalid recipient";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sendMessage: function(message) {
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
var m = Whisper.Messages.addOutgoingMessage(message, this);
|
||||||
|
textsecure.sendMessage(this.get('recipients'), m.toProto(),
|
||||||
|
function(result) {
|
||||||
|
console.log(result);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
messages: function() {
|
||||||
|
return Whisper.Messages.where({threadId: this.id});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Whisper.Threads = new (Backbone.Collection.extend({
|
||||||
|
localStorage: new Backbone.LocalStorage("Threads"),
|
||||||
|
model: Thread,
|
||||||
|
comparator: 'timestamp',
|
||||||
|
findOrCreate: function(attributes) {
|
||||||
|
var thread = Whisper.Threads.add(attributes, {merge: true});
|
||||||
|
thread.save();
|
||||||
|
return thread;
|
||||||
|
},
|
||||||
|
|
||||||
|
findOrCreateForRecipients: function(recipients) {
|
||||||
|
var attributes = {};
|
||||||
|
if (recipients.length > 1) {
|
||||||
|
attributes = {
|
||||||
|
//TODO group id formatting?
|
||||||
|
name : recipients,
|
||||||
|
recipients : recipients,
|
||||||
|
type : 'group',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
attributes = {
|
||||||
|
id : recipients[0],
|
||||||
|
name : recipients[0],
|
||||||
|
recipients : recipients,
|
||||||
|
type : 'private',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return this.findOrCreate(attributes);
|
||||||
|
},
|
||||||
|
|
||||||
|
findOrCreateForIncomingMessage: function(decrypted) {
|
||||||
|
var attributes = {};
|
||||||
|
if (decrypted.message.group) {
|
||||||
|
attributes = {
|
||||||
|
id : decrypted.message.group.id,
|
||||||
|
name : decrypted.message.group.name,
|
||||||
|
recipients : decrypted.message.group.members,
|
||||||
|
type : 'group',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
attributes = {
|
||||||
|
id : decrypted.pushMessage.source,
|
||||||
|
name : decrypted.pushMessage.source,
|
||||||
|
recipients : [decrypted.pushMessage.source],
|
||||||
|
type : 'private'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return this.findOrCreate(attributes);
|
||||||
|
}
|
||||||
|
}))();
|
||||||
|
})();
|
@ -0,0 +1,116 @@
|
|||||||
|
var Whisper = Whisper || {};
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var destroyer = Backbone.View.extend({
|
||||||
|
tagName: 'button',
|
||||||
|
className: 'btn btn-square btn-sm destroy',
|
||||||
|
initialize: function() {
|
||||||
|
this.$el.html('×');
|
||||||
|
this.$el.click(this.destroy.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy: function() {
|
||||||
|
_.each(this.model.messages(), function(message) { message.destroy(); });
|
||||||
|
this.model.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var menu = Backbone.View.extend({
|
||||||
|
tagName: 'ul',
|
||||||
|
className: 'menu',
|
||||||
|
initialize: function() {
|
||||||
|
this.$el.html("<li>delete</li>");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Whisper.ConversationView = Backbone.View.extend({
|
||||||
|
tagName: 'li',
|
||||||
|
className: 'conversation',
|
||||||
|
|
||||||
|
initialize: function() {
|
||||||
|
this.listenTo(this.model, 'change', this.render); // auto update
|
||||||
|
this.listenTo(this.model, 'message', this.addMessage); // auto update
|
||||||
|
this.listenTo(this.model, 'destroy', this.remove); // auto update
|
||||||
|
this.listenTo(this.model, 'select', this.open);
|
||||||
|
|
||||||
|
this.$el.addClass('closed');
|
||||||
|
this.$destroy = (new destroyer({model: this.model})).$el;
|
||||||
|
|
||||||
|
this.$image = $('<div class="image">');
|
||||||
|
this.$name = $('<span class="name">');
|
||||||
|
this.$header = $('<div class="header">').append(this.$image, this.$name);
|
||||||
|
|
||||||
|
this.$button = $('<button class="btn">').append($('<span>').text('Send'));
|
||||||
|
this.$input = $('<input type="text">').attr('autocomplete','off');
|
||||||
|
this.$form = $("<form class=''>").append(this.$input);
|
||||||
|
|
||||||
|
this.$messages = $('<ul class="messages">');
|
||||||
|
this.$collapsable = $('<div class="collapsable">').hide();
|
||||||
|
this.$collapsable.append(this.$messages, this.$form);
|
||||||
|
|
||||||
|
this.$el.append(this.$destroy, this.$header, this.$collapsable);
|
||||||
|
this.addAllMessages();
|
||||||
|
|
||||||
|
this.$form.submit(function(input,thread){ return function(e) {
|
||||||
|
if (!input.val().length) { return false; }
|
||||||
|
thread.sendMessage(input.val());
|
||||||
|
input.val("");
|
||||||
|
e.preventDefault();
|
||||||
|
};}(this.$input, this.model));
|
||||||
|
|
||||||
|
this.$header.click(function(e) {
|
||||||
|
var $conversation = $(e.target).closest('.conversation');
|
||||||
|
if (!$conversation.hasClass('closed')) {
|
||||||
|
$conversation.addClass('closed');
|
||||||
|
$conversation.find('.collapsable').slideUp(600);
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$button.click(function(button,input,thread){ return function(e) {
|
||||||
|
if (!input.val().length) { return false; }
|
||||||
|
button.attr("disabled", "disabled");
|
||||||
|
button.find('span').text("Sending");
|
||||||
|
|
||||||
|
thread.sendMessage(input.val()).then(function(){
|
||||||
|
button.removeAttr("disabled");
|
||||||
|
button.find('span').text("Send");
|
||||||
|
});
|
||||||
|
|
||||||
|
input.val("");
|
||||||
|
};}(this.$button, this.$input, this.model));
|
||||||
|
|
||||||
|
this.$el.click(this.open.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
remove: function() {
|
||||||
|
this.$el.remove();
|
||||||
|
},
|
||||||
|
|
||||||
|
open: function(e) {
|
||||||
|
if (this.$el.hasClass('closed')) {
|
||||||
|
this.$el.removeClass('closed');
|
||||||
|
this.$collapsable.slideDown(600);
|
||||||
|
}
|
||||||
|
this.$input.focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
addMessage: function (message) {
|
||||||
|
var view = new Whisper.MessageView({ model: message });
|
||||||
|
this.$messages.append(view.render().el);
|
||||||
|
},
|
||||||
|
|
||||||
|
addAllMessages: function () {
|
||||||
|
_.each(this.model.messages(), this.addMessage, this);
|
||||||
|
this.render();
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
this.$name.text(this.model.get('name'));
|
||||||
|
this.$image.css('background-image: ' + this.model.get('image') + ';');
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
@ -0,0 +1,64 @@
|
|||||||
|
var Whisper = Whisper || {};
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Destroyer = Backbone.View.extend({
|
||||||
|
tagName: 'button',
|
||||||
|
className: 'btn btn-square btn-sm',
|
||||||
|
initialize: function() {
|
||||||
|
this.$el.html('×');
|
||||||
|
this.listenTo(this.$el, 'click', this.model.destroy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Whisper.MessageView = Backbone.View.extend({
|
||||||
|
tagName: "li",
|
||||||
|
className: "message",
|
||||||
|
|
||||||
|
initialize: function() {
|
||||||
|
this.$el.
|
||||||
|
append($('<div class="bubble">').
|
||||||
|
append(
|
||||||
|
$('<span class="message-text">'),
|
||||||
|
$('<span class="message-attachment">'),
|
||||||
|
$('<span class="metadata">')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.$el.addClass(this.model.get('type'));
|
||||||
|
this.listenTo(this.model, 'change', this.render); // auto update
|
||||||
|
this.listenTo(this.model, 'destroy', this.remove); // auto update
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
this.$el.find('.message-text').text(this.model.get('body'));
|
||||||
|
var attachments = this.model.get('attachments');
|
||||||
|
if (attachments) {
|
||||||
|
for (var i = 0; i < attachments.length; i++)
|
||||||
|
this.$el.find('.message-attachment').append('<img src="' + attachments[i] + '" />');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$el.find('.metadata').text(this.formatTimestamp());
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
remove: function() {
|
||||||
|
this.$el.remove();
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTimestamp: function() {
|
||||||
|
var timestamp = this.model.get('timestamp');
|
||||||
|
var now = new Date().getTime();
|
||||||
|
var date = new Date();
|
||||||
|
date.setTime(timestamp*1000);
|
||||||
|
if (now - timestamp > 60*60*24*7) {
|
||||||
|
return date.toLocaleDateString('en-US',{month: 'short', day: 'numeric'});
|
||||||
|
}
|
||||||
|
if (now - timestamp > 60*60*24) {
|
||||||
|
return date.toLocaleDateString('en-US',{weekday: 'short'});
|
||||||
|
}
|
||||||
|
return date.toTimeString();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
@ -0,0 +1,34 @@
|
|||||||
|
var Whisper = Whisper || {};
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// This is an ephemeral collection of global notification messages to be
|
||||||
|
// presented in some nice way to the user. In this case they will fade in/out
|
||||||
|
// one at a time.
|
||||||
|
|
||||||
|
var queue = new Backbone.Collection();
|
||||||
|
var view = new (Backbone.View.extend({
|
||||||
|
className: 'help',
|
||||||
|
initialize: function() {
|
||||||
|
this.$el.appendTo($('body'));
|
||||||
|
this.listenToOnce(queue, 'add', this.presentNext);
|
||||||
|
},
|
||||||
|
presentNext: function() {
|
||||||
|
var next = queue.shift();
|
||||||
|
if (next) {
|
||||||
|
this.$el.text(next.get('message')).fadeIn(this.setFadeOut.bind(this));
|
||||||
|
} else {
|
||||||
|
this.listenToOnce(queue, 'add', this.presentNext);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setFadeOut: function() {
|
||||||
|
setTimeout(this.fadeOut.bind(this), 1500);
|
||||||
|
},
|
||||||
|
fadeOut: function() {
|
||||||
|
this.$el.fadeOut(this.presentNext.bind(this));
|
||||||
|
},
|
||||||
|
}))();
|
||||||
|
|
||||||
|
Whisper.notify = function(str) { queue.add({message: str}); }
|
||||||
|
|
||||||
|
})();
|
Loading…
Reference in New Issue