diff --git a/server/camlistored/ui/blob_item_foursquare_content.js b/server/camlistored/ui/blob_item_foursquare_content.js
index 3d3ae35c7..8aaeb8d31 100644
--- a/server/camlistored/ui/blob_item_foursquare_content.js
+++ b/server/camlistored/ui/blob_item_foursquare_content.js
@@ -21,6 +21,7 @@ goog.require('goog.math.Size');
goog.require('goog.object');
goog.require('goog.string');
+goog.require('cam.dateUtils');
goog.require('cam.math');
goog.require('cam.permanodeUtils');
goog.require('cam.Thumber');
@@ -55,38 +56,10 @@ cam.BlobItemFoursquareContent = React.createClass({
)
)
),
- React.DOM.div({className:'cam-blobitem-fs-checkin-when'}, this.formatDate_())
+ React.DOM.div({className:'cam-blobitem-fs-checkin-when'}, cam.dateUtils.formatDateShort(this.props.date))
)
);
},
-
- formatDate_: function() {
- var seconds = Math.floor((Date.now() - this.props.date) / 1000);
- var interval = Math.floor(seconds / 31536000);
-
- return (function() {
- if (interval > 1) {
- return interval + ' years';
- }
- interval = Math.floor(seconds / 2592000);
- if (interval > 1) {
- return interval + ' months';
- }
- interval = Math.floor(seconds / 86400);
- if (interval > 1) {
- return interval + ' days';
- }
- interval = Math.floor(seconds / 3600);
- if (interval > 1) {
- return interval + ' hours';
- }
- interval = Math.floor(seconds / 60);
- if (interval > 1) {
- return interval + ' minutes';
- }
- return Math.floor(seconds) + ' seconds';
- })() + ' ago';
- },
});
// Blech, we need this to prevent images from flashing when data changes server-side.
diff --git a/server/camlistored/ui/blob_item_twitter.css b/server/camlistored/ui/blob_item_twitter.css
new file mode 100644
index 000000000..a58eea73c
--- /dev/null
+++ b/server/camlistored/ui/blob_item_twitter.css
@@ -0,0 +1,63 @@
+/*
+Copyright 2014 The Camlistore Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.cam-blobitem-twitter-tweet {
+ color: black;
+ display: block;
+ font-size: 90%;
+ position: relative;
+ overflow: hidden;
+ white-space: normal;
+ border-radius: 7%;
+}
+
+.cam-blobitem-twitter-tweet table {
+ border-spacing: 0;
+ background-color: #e1e8ed;
+ width: 100%;
+ height: 100%;
+}
+
+.cam-blobitem-twitter-tweet-icon {
+ position: absolute;
+ width: 100%;
+ bottom: 0;
+}
+
+.cam-blobitem-twitter-tweet-icon img {
+ width: 4em;
+ height: 4em;
+ position: absolute;
+ bottom: 1em;
+ right: 1em;
+ opacity: 1;
+}
+
+.cam-blobitem-twitter-tweet-meta {
+ text-align: left;
+ vertical-align: top;
+ padding: 0.8em;
+}
+
+.cam-blobitem-twitter-tweet-date {
+ color: #aaa;
+}
+
+.cam-blobitem-twitter-tweet-image {
+ background-position: top;
+ background-size: cover;
+ height: 100%;
+}
diff --git a/server/camlistored/ui/blob_item_twitter_content.js b/server/camlistored/ui/blob_item_twitter_content.js
new file mode 100644
index 000000000..60f24453c
--- /dev/null
+++ b/server/camlistored/ui/blob_item_twitter_content.js
@@ -0,0 +1,122 @@
+/*
+Copyright 2014 The Camlistore Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+goog.provide('cam.BlobItemTwitterContent');
+
+goog.require('goog.math.Size');
+
+goog.require('cam.dateUtils');
+goog.require('cam.math');
+goog.require('cam.permanodeUtils');
+goog.require('cam.Thumber');
+
+cam.BlobItemTwitterContent = React.createClass({
+ propTypes: {
+ date: React.PropTypes.number.isRequired,
+ href: React.PropTypes.string.isRequired,
+ image: React.PropTypes.string,
+ size: React.PropTypes.instanceOf(goog.math.Size).isRequired,
+ username: React.PropTypes.string.isRequired,
+ },
+
+ getImageRow_: function() {
+ if (!this.props.image) {
+ return null;
+ }
+
+ return React.DOM.tr(null,
+ React.DOM.td({
+ className: 'cam-blobitem-twitter-tweet-image',
+ colSpan: 2,
+ src: 'twitter-icon.png',
+ style: {
+ backgroundImage: 'url(' + this.props.image + ')',
+ },
+ })
+ );
+ },
+
+ render: function() {
+ return React.DOM.a({
+ href: this.props.href,
+ className: 'cam-blobitem-twitter-tweet',
+ style: {
+ width: this.props.size.width,
+ height: this.props.size.height,
+ },
+ },
+ React.DOM.table({height: this.props.image ? '100%' : ''},
+ React.DOM.tr(null,
+ React.DOM.td({className: 'cam-blobitem-twitter-tweet-meta'},
+ React.DOM.span({className: 'cam-blobitem-twitter-tweet-date'}, cam.dateUtils.formatDateShort(this.props.date)),
+ React.DOM.br(),
+ React.DOM.span({className: ' cam-blobitem-twitter-tweet-content'}, this.props.content)
+ )
+ ),
+ this.getImageRow_(),
+ React.DOM.tr(null,
+ React.DOM.td({className: 'cam-blobitem-twitter-tweet-icon'},
+ React.DOM.img({src: 'twitter-logo.png'})
+ )
+ )
+ )
+ );
+ },
+});
+
+cam.BlobItemTwitterContent.getHandler = function(blobref, searchSession, href) {
+ var m = searchSession.getMeta(blobref);
+ if (m.camliType != 'permanode') {
+ return null;
+ }
+
+ if (cam.permanodeUtils.getSingleAttr(m.permanode, 'camliNodeType') != 'twitter.com:tweet') {
+ return null;
+ }
+
+ var content = cam.permanodeUtils.getSingleAttr(m.permanode, 'content');
+ var date = cam.permanodeUtils.getSingleAttr(m.permanode, 'startDate');
+ var username = cam.permanodeUtils.getSingleAttr(m.permanode, 'url').match(/^https:\/\/twitter.com\/(.+?)\//)[1];
+ var imageMeta = cam.permanodeUtils.getSingleAttr(m.permanode, 'camliContentImage');
+ if (imageMeta) {
+ imageMeta = searchSession.getResolvedMeta(imageMeta);
+ }
+
+ return new cam.BlobItemTwitterContent.Handler(content, Date.parse(date), href, imageMeta, username);
+};
+
+cam.BlobItemTwitterContent.Handler = function(content, date, href, imageMeta, username) {
+ this.content_ = content;
+ this.date_ = date;
+ this.href_ = href;
+ this.username_ = username;
+ this.thumber_ = imageMeta ? new cam.Thumber.fromImageMeta(imageMeta) : null;
+};
+
+cam.BlobItemTwitterContent.Handler.prototype.getAspectRatio = function() {
+ return 1.0;
+};
+
+cam.BlobItemTwitterContent.Handler.prototype.createContent = function(size) {
+ return cam.BlobItemTwitterContent({
+ content: this.content_,
+ date: this.date_,
+ href: this.href_,
+ image: this.thumber_ ? this.thumber_.getSrc(size) : null,
+ size: size,
+ username: this.username_,
+ });
+};
diff --git a/server/camlistored/ui/date_utils.js b/server/camlistored/ui/date_utils.js
new file mode 100644
index 000000000..7dc839d57
--- /dev/null
+++ b/server/camlistored/ui/date_utils.js
@@ -0,0 +1,47 @@
+/*
+Copyright 2014 The Camlistore Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+goog.provide('cam.dateUtils');
+
+cam.dateUtils.formatDateShort = function(date) {
+ // TODO(aa): Do something better based on Closure date/i18n utils.
+ // I think I would prefer this to return (in en-us) either '11:18 PM', 'Jun 11', or 'June 11 1952', depending on how far back it is. I don't find '5 hours ago' that useful.
+ var seconds = Math.floor((Date.now() - date) / 1000);
+ var interval = Math.floor(seconds / 31536000);
+
+ return (function() {
+ if (interval > 1) {
+ return interval + ' years';
+ }
+ interval = Math.floor(seconds / 2592000);
+ if (interval > 1) {
+ return interval + ' months';
+ }
+ interval = Math.floor(seconds / 86400);
+ if (interval > 1) {
+ return interval + ' days';
+ }
+ interval = Math.floor(seconds / 3600);
+ if (interval > 1) {
+ return interval + ' hours';
+ }
+ interval = Math.floor(seconds / 60);
+ if (interval > 1) {
+ return interval + ' minutes';
+ }
+ return Math.floor(seconds) + ' seconds';
+ })() + ' ago';
+};
diff --git a/server/camlistored/ui/index.html b/server/camlistored/ui/index.html
index 6a279a1ca..4f761c557 100644
--- a/server/camlistored/ui/index.html
+++ b/server/camlistored/ui/index.html
@@ -40,6 +40,7 @@ limitations under the License.
+
diff --git a/server/camlistored/ui/index.js b/server/camlistored/ui/index.js
index c7b72af41..7ca17fc7d 100644
--- a/server/camlistored/ui/index.js
+++ b/server/camlistored/ui/index.js
@@ -26,11 +26,12 @@ goog.require('goog.Uri');
goog.require('cam.BlobDetail');
goog.require('cam.BlobItemContainerReact');
+goog.require('cam.BlobItemDemoContent');
goog.require('cam.BlobItemFoursquareContent');
goog.require('cam.BlobItemGenericContent');
-goog.require('cam.BlobItemVideoContent');
goog.require('cam.BlobItemImageContent');
-goog.require('cam.BlobItemDemoContent');
+goog.require('cam.BlobItemTwitterContent');
+goog.require('cam.BlobItemVideoContent');
goog.require('cam.ContainerDetail');
goog.require('cam.DetailView');
goog.require('cam.DirectoryDetail');
@@ -53,12 +54,14 @@ cam.IndexPage = React.createClass({
RAW: 'raw'
},
+ // Note that these are ordered by priority.
BLOB_ITEM_HANDLERS_: [
cam.BlobItemDemoContent.getHandler,
cam.BlobItemFoursquareContent.getHandler,
+ cam.BlobItemTwitterContent.getHandler,
cam.BlobItemImageContent.getHandler,
cam.BlobItemVideoContent.getHandler,
- cam.BlobItemGenericContent.getHandler // must be last
+ cam.BlobItemGenericContent.getHandler
],
propTypes: {
diff --git a/server/camlistored/ui/twitter-logo.png b/server/camlistored/ui/twitter-logo.png
new file mode 100644
index 000000000..1b1562d43
Binary files /dev/null and b/server/camlistored/ui/twitter-logo.png differ