Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Add CocoaPush support to CocoaPods.org #36

Closed
wants to merge 14 commits into from

5 participants

@swizzlr

-- DO NOT MERGE --

This pull request is for discussion and implementation of a user interface for CocoaPush.

As I know nothing about design or frontend JS, I would appreciate input as to how it might work.

My current thinking is that the results, when hovered over, could have a button slide out that says "notify me". This will then add the pod to the user's list of desired pods, and, if necessary, initiate the request for permissions etc.

Through a runtime check for safari push notification permissions we can display a link at the top of the page, or on the page, to a small panel allowing the user to manage their settings: namely, to remove registered pods. Deregistration from push notifications should be done through Safari preferences.

This user settings page is keyed, unfortunately, on a per computer basis. Without a user account framework we are unable to share notifications or their settings between computers, but I believe the simplicity of no login is worth the tradeoff, both in technical debt and user experience.

cc @orta, as design dictator, and anyone else who knows JS.

Refer to this issue for tracking within cocoapush.

@orta
Owner

so, how I had imagined it:

If it's not been already been selected it stays a 50% opacity
screen shot 2014-01-24 at 15 47 15

hovering over it turns it 100% opacity and brings over a popover telling you what it'll do
screen shot 2014-01-24 at 15 51 09

clicking it makes the star dark ( and will register etc ) and then it switches to telling you what you have already starred
screen shot 2014-01-24 at 15 57 57

@orta
Owner

its a rough spec, and once there's something there then we can polish etc. Stars shouldn't show on non-safari browsers IMO, bit of a dick move to say "you should load this in safari" it's not 2002.

@swizzlr

Yep, definitely. What JS framework do we use?

@orta
Owner

jQuery, no frameworks. There's a template file here you can work from: https://github.com/CocoaPods/cocoapods.org/blob/master/source/_search-templates.html.slim

@swizzlr

Where did you get the stars from?

@swizzlr

screen shot 2014-01-25 at 11 04 51 pm
It's a start, though I'll need to fix the padding.

@swizzlr

Okay, as I have stubbed and commented, I'd like to make the stars rotate while a network request is processing – do you think that's a good idea?

If so, how would I go about animating it?

@floere
Owner

Can I suggest you move this from the search specific configuration to a separate JS file?

Sure, how do I include it?

@swizzlr

@floere This is what you meant, yeah?

Owner

@swizzlr Exactly :)

@orta
Owner

wanna hack on this some this weekend or some evening @swizzlr ? ( /cc @segiddins you're welcome t come along too? )

@segiddins
Owner

I'd be more than happy to!

@swizzlr

@segiddins @orta Totally – this weekend perhaps? I'm pretty rammed this week.

@orta
Owner

The weekend is good for me, I have no plans, saturday midday at Artsy HQ?

@swizzlr

@orta @segiddins on for this

@kylef
Owner

What's the state of this? /cc @swizzlr

@orta
Owner

Closing, happy to go through and the this running again but the site has moved considerably since this PR.

@orta orta closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
7 Gemfile
@@ -1,5 +1,10 @@
# If you have OpenSSL installed, we recommend updating
# the following line to use "https"
+
+# RVM support
+#ruby=ruby-2.1.0
+#ruby-gemset=cocoapods.org
+
source 'https://rubygems.org'
gem "middleman", "~>3.0.13"
@@ -21,4 +26,4 @@ gem 'pygments.rb'
gem 'github-markup'
gem "rest"
-gem "twitter"
+gem "twitter"
2  shared
@@ -1 +1 @@
-Subproject commit c2b49788cdc062db42df61c2fc8708ab4e4fd881
+Subproject commit 150ae916e3b7663f064d2d4f63387d8cb0fde927
View
13 source/_search-templates.html.slim
@@ -3,6 +3,9 @@
ruby:
popover_html = "<h4 class='has-flash'>Copy to clipboard</h4><h4 class='no-flash'>For your <a href='http://guides.cocoapods.org/using/the-podfile.html'>Podfile</a></h4><pre><code>pod '{{ id }}', '~> {{ version }}'</code></pre><input class='no-flash' value=\"pod '{{ id }}', '~> {{ version }}'\" type=text>"
+ fave_info = "<h4 class='has-safari-push'>Get Push Notifications for this pod</h4>"
+ fave_selected = "<h4 class='has-enabled-push-here'></h4>"
+
script id="search_result" type="text/html"
li.result
@@ -10,17 +13,19 @@ script id="search_result" type="text/html"
h3
a href="{{ link }}" {{ id }}
span.version {{ version }}
- img.copy src="./images/copy-to-clipboard.png" data-clipboard-text="pod '{{ id }}', '~> {{ version }}'" data-toggle="popover" data-placement="top" data-container="body" data-html="true" data-content=popover_html
+ img.fave src="./images/star-hollow.png" data-toggle="popover" data-placement="top" data-container="body" data-html="true" data-content=fave_info pod-name="{{ id }}" is_faved="{{ is_faved }}"
+
+ img.copy src="./images/copy-to-clipboard.png" data-clipboard-text="pod '{{ id }}', '~> {{ version }}'" data-toggle="popover" data-placement="top" data-container="body" data-html="true" data-content=popover_html
| {{#platform}}
span.os {{ platform }}
| {{/platform}}
-
+
p {{ summary }}
p.author {{ authors }}
-
+
div.actions.col-lg-4.col-sm-5.col-xs-12
div.action-wrapper
a href="{{ site_link }}" Site
a href="{{ docs_link }}" Docs
- a href="{{ spec_link }}" Spec
+ a href="{{ spec_link }}" Spec
View
BIN  source/images/star-filled.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  source/images/star-hollow.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
5 source/javascripts/application.js
@@ -7,6 +7,7 @@
*= require history.adapter.jquery.min.js
*= require picky.min.js
*= require search.config.js
+*= require cocoapush.js
*= require zero-clipboard.min.js
*= require has_flash.js
*= require ICanHaz.js
@@ -21,14 +22,14 @@ $(document).bind("touchmove", function(e){
$( document ).ready( function(){
$('.underscore a[data-toggle="tab"]').on('show.bs.tab', function (e) {
-
+
var index = $(e.target.parentElement).index()
var width = $(window).width();
var constant = (width < 768) ? 33: 40;
var percent = (index * constant).toString()
$("#homepage-tab-indicator").css("margin-left", percent + "%");
})
-
+
if( hasFlash() ) {
$("html").addClass("flash")
}
View
100 source/javascripts/cocoapush.js
@@ -0,0 +1,100 @@
+//singleton representing the CocoaPush webservice.
+//If push notifications are not supported, returns 'NOT SUPPORTED'
+//If push notifications have been requested and denied, returns 'NOT ALLOWED'
+//Consumer of this singleton is responsible for updating UI in response to the former two cases
+var CocoaPush = new function () {
+ this.enabled = true;
+
+ //param: callback taking permission data as parameter
+ //return: null
+ this.permissionData = function(cb) {
+ var permissionData = ('safari' in window && 'pushNotification' in window.safari) ? window.safari.pushNotification.permission('web.org.cocoapods.push') : { permission: 'denied', deviceToken: null };
+ if (permissionData.permission === 'default') {
+ // This is a new web service URL and its validity is unknown.
+ window.safari.pushNotification.requestPermission(
+ 'https://localhost:3000/push', // The web service URL.
+ 'web.org.cocoapods.push', // The Website Push ID.
+ {}, // Data that you choose to send to your server to help you identify the user.
+ function (permission) { // The callback function.
+ permissionData = permission; //update the object var with new info to fall through to bottom two cases
+ cb(permission);
+ }
+ );
+ } else {
+ cb(permissionData);
+ }
+ return;
+ };
+
+ this.pods = [];
+ var podsInitialized = false;
+ // get pods. takes a callback which has one parameter, the array of pods
+ this.getPods = function (cb) {
+ if (!podsInitialized) {
+ this.permissionData(function(permission) {
+ $.get("https://localhost:3000/push/v1/settingsForDeviceToken/" + permission.deviceToken, null, null, 'JSON')
+ .done(function(settings) {
+ CocoaPush.pods = settings["pods"];
+ podsInitialized = true;
+ cb(CocoaPush.pods);
+ })
+ .fail(function() {
+ cb(CocoaPush.pods);
+ });
+ });
+ } else {
+ cb(this.pods);
+ }
+ };
+
+ //function to upload new pod settings array, and keep state of CocoaPush consistent
+ //takes the array to upload, the array to store in case it fails, and a callback that is supplied with true or false depending on success or fail
+ function uploadPodSettings(cb, newPodArray, oldPodArray) {
+ CocoaPush.permissionData(function (permission) {
+ $.ajax("https://localhost:3000/push/v1/settingsForDeviceToken/" + permission.deviceToken, //send up and
+ {
+ data: JSON.stringify({"pods": newPodArray}),
+ type: 'POST',
+ processData: false,
+ error: function () {
+ CocoaPush.pods = oldPodArray;
+ cb(false);
+ },
+ success: function () {
+ CocoaPush.pods = newPodArray;
+ cb(true);
+ }
+ }
+ )
+ });
+ }
+
+ // add pod to remote webservice, update local cache
+ // takes the pod name as a string, and a callback receiving a boolean value indicating whether it was successfully stored
+ this.addPod = function (pod, cb) {
+ this.getPods(function(pods) {
+ if (pods.indexOf(pod) != -1) { //already present locally, so no worries
+ cb(true);
+ } else {
+ var newPodArray = CocoaPush.pods.slice(0);
+ newPodArray.push(pod);
+ uploadPodSettings(cb, newPodArray, CocoaPush.pods.slice(0));
+ }
+ })
+ };
+
+ // remove pod from CocoaPush, update local cache
+ // takes the pod name as a string, and a callback receiving a boolean value indicating whether it was successfully removed
+ this.removePod = function (pod, cb) {
+ this.getPods(function(pods) {
+ if (pods.indexOf(pod) == -1) {
+ cb(true);
+ } else {
+ var oldPodArray = CocoaPush.pods.slice(0);
+ var newPodArray = oldPodArray.filter(function (obj) { return (obj !== pod); });
+ uploadPodSettings(cb, newPodArray, oldPodArray);
+ }
+ })
+ }
+
+};
View
166 source/javascripts/search.config.js
@@ -1,15 +1,49 @@
$(window).ready(function() {
var onMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
-
+
var searchInput = $('#search input[type="search"]');
var helpText = $('#search fieldset p');
-
+
var platformRemoverRegexp = /\b(platform|on\:\w+\s?)+/;
var platformSelect = $("#search_results div.platform");
-
+
var allocationSelect = $('#search_results div.allocations');
var resultsContainer = $('#results_container');
-
+
+ function fillStar(target) {
+ target.setAttribute('src', './images/star-filled.png');
+ target.setAttribute('is_faved', true);
+ }
+
+ function favePod(event) {
+ CocoaPush.addPod(event.target.attributes['pod-name'].value, function(success) {
+ //stopStarSpin(event.target);
+ if (success) {
+ fillStar(event.target);
+ } else {
+ // present an alert?
+ }
+ });
+ //spinStar(event.target);
+ }
+
+ function hollowStar(target) {
+ target.setAttribute('src', './images/star-hollow.png');
+ target.setAttribute('is_faved', false);
+ }
+
+ function unFavePod(event) {
+ CocoaPush.removePod(event.target.attributes['pod-name'].value, function(success) {
+ //stopStarSpin(event.target);
+ if (success) {
+ hollowStar(event.target);
+ } else {
+ // present an alert?
+ }
+ });
+ //spinStar(event.target);
+ }
+
// Tracking the search results.
//
var trackAnalytics = function(data, query) {
@@ -20,32 +54,32 @@ $(window).ready(function() {
_gaq.push(['_trackEvent', 'search', 'not found', query, 0]);
}
}
-
+
// Tracking platform selection.
//
var trackPlatformSelection = function() {
_gaq.push(['_trackEvent', 'platform', 'switch platform', platformSelect.find('input:checked').val(), 1]);
}
-
+
// Tracking category/categories selection.
//
var trackAllocationSelection = function(category, count) {
_gaq.push(['_trackEvent', 'allocation', 'filter categories', category, count]);
}
-
+
// Tracking category/categories selection.
//
var trackResultLinkSelection = function(href) {
_gaq.push(['_trackEvent', 'resultlink', 'click outbound link', href, 1]);
}
-
+
// Sets the checkbox labels correctly.
//
var selectCheckedPlatform = function() {
platformSelect.find('label').removeClass('selected');
platformSelect.find('input:checked + label').addClass('selected');
};
-
+
// Hide the header.
//
var headerHidden = false;
@@ -57,7 +91,7 @@ $(window).ready(function() {
headerHidden = true;
}
};
-
+
// Show the header.
//
var showHeader = function() {
@@ -68,7 +102,7 @@ $(window).ready(function() {
headerHidden = false;
}
};
-
+
// Reset the search interface to its initial configuration.
//
var resetSearchInterface = function() {
@@ -78,31 +112,31 @@ $(window).ready(function() {
allocationSelect.hide();
$('#search_results div.results').hide();
};
-
+
//
//
var prepareSearchInterfaceForResults = function() {
hideHeader();
$('#search span.amount').show();
};
-
+
var resultsSearchInterface = function() {
platformSelect.show();
allocationSelect.show();
// $('#search div.results').show(); // Picky does this already.
};
-
+
var removePlatform = function(query) {
return query.replace(platformRemoverRegexp, '');
};
-
+
//
//
var noResultsSearchInterface = function(query) {
// $('#search_results .no_results').show(); // Picky does this already.
platformSelect.show();
allocationSelect.hide();
-
+
// Get special no_results hash from the search API:
// * autosplit query
// * tags
@@ -113,14 +147,14 @@ $(window).ready(function() {
$.getJSON('http://search.cocoapods.org/no_results.json', 'query=' + removePlatform(query), function(data, textStatus, jqXHR) {
var suggested_query = data.split[0].join(' ');
var total = data.split[1];
-
+
var splitsContainer = $('#results_container .no_results .splits');
if (suggested_query && total > 0) {
splitsContainer.html("<p>We found " + total + " results searching for <a href='javascript:pickyClient.insert(\"" + suggested_query + "\");'>" + suggested_query + "</a>.</p>")
} else {
splitsContainer.html('');
}
-
+
var tagsContainer = $('#results_container .no_results .tags');
var tags = [];
$.each(data.tag, function(name, amount) {
@@ -130,7 +164,7 @@ $(window).ready(function() {
tagsContainer.find('p').append(tags.sort().join(', ')).append('.');
});
};
-
+
// Renders an entry, then returns the rendered HTML.
//
// TODO Improve. This is just a quick prototype.
@@ -140,41 +174,46 @@ $(window).ready(function() {
osx: 'OS X'
};
var goodSource = /^http/;
-
+
var extractRepoFromSource = function(entry) {
var link, value;
var source = entry.source;
for (var key in source) {
if (key == 'http') { return ''; }
-
+
value = source[key];
if (value.toString().match(goodSource)) { link = value; break; }
}
return link;
};
-
+
var render = function(entry) {
entry.platform = platformMapping[entry.platforms];
entry.authors = $.map(entry.authors, function(email, name) {
return '<a href="javascript:pickyClient.insert(\'' + name.replace(/[']/, "\\\\\'") + '\')">' + name + '</a>';
});
-
+
entry.docs_link = entry.documentation_url || 'http://cocoadocs.org/docsets/' + entry.id + '/' + entry.version;
- entry.site_link = entry.link || extractRepoFromSource(entry)
+ entry.site_link = entry.link || extractRepoFromSource(entry)
entry.spec_link = 'https://github.com/CocoaPods/Specs/tree/master/' + entry.id + '/' + entry.version + '/' + entry.id + '.podspec'
-
+
+ if (CocoaPush.enabled) {
+ CocoaPush.getPods(function(pods) {
+ entry.is_faved = (pods.indexOf(entry.id) !== -1); //if pod is in list of pods, then it must be faved
+ })
+ }
// render with ICanHaz, see _search-templates
return ich.search_result(entry, true)
};
-
+
// The search uses the convenience API on search.cocoapods.org.
//
var searchURL = 'http://search.cocoapods.org/api/v1/pods.picky.hash.json';
-
+
pickyClient = new PickyClient({
full: searchURL,
fullResults: 20,
-
+
// The live query does a full query.
//
live: searchURL,
@@ -196,7 +235,7 @@ $(window).ready(function() {
hiddenAllocations: '#search_results div.allocations .onrequest',
counterSelector: '#search form span.amount',
moreSelector: '#search_results .allocations .more',
-
+
// Before a query is inserted into the search field
// we clean it of any platform terms.
//
@@ -225,7 +264,7 @@ $(window).ready(function() {
// We don't add the platform if it is empty (still saved in history as empty, though).
//
if (query == '') { return ''; }
-
+
// Otherwise we add in the platform.
//
query = query.replace(platformRemoverRegexp, '');
@@ -237,7 +276,7 @@ $(window).ready(function() {
// Track query for analytics.
//
trackAnalytics(data, query);
-
+
// If somebody cleared the search input, do not show any results
// arriving "late" (well, slower than the person can press backspace).
//
@@ -245,7 +284,7 @@ $(window).ready(function() {
resetSearchInterface();
return false;
}
-
+
// If no results are found.
//
if (0 == data.total) {
@@ -253,7 +292,7 @@ $(window).ready(function() {
} else {
resultsSearchInterface();
}
-
+
// Render the JSON into HTML.
//
var allocations = data.allocations;
@@ -262,7 +301,7 @@ $(window).ready(function() {
return render(entry);
});
});
-
+
return data;
},
// After Picky has handled the data and updated the view.
@@ -271,7 +310,7 @@ $(window).ready(function() {
// Install Popovers for the copy to clipboard
// depending on whether ZeroClipboard succeeds
- $copy_to_clipboard = $('ol.results img.copy')
+ $copy_to_clipboard = $('ol.results img.copy')
var clip = new ZeroClipboard(
$copy_to_clipboard, {
@@ -279,13 +318,13 @@ $(window).ready(function() {
forceHandCursor: true
}
);
-
+
clip.on( 'noflash', function ( client, args ) {
-
+
// provide a recursive wait method
// that checks for the hover on the popover/clipboard
// before hiding so you can select text
-
+
function closePopoverForNode(node){
setTimeout(function() {
if (!$(node).is(':hover') && !$(".popover:hover").length) {
@@ -295,17 +334,17 @@ $(window).ready(function() {
}
}, 500);
}
-
+
// With no flash you should be able to select the text
// in the popover
-
+
$copy_to_clipboard.popover({
trigger: "manual",
container: "body"
-
+
}).on("click", function(e) {
e.preventDefault();
-
+
}).on("mouseenter", function() {
$(this).popover("show");
$(".popover input").select()
@@ -322,16 +361,31 @@ $(window).ready(function() {
$("h4.has-flash").text("Saved to clipboard");
$(".popover").addClass("saved")
});
-
+
clip.on( 'mouseover', function ( client, args ) {
$(this).popover('show')
});
-
+
clip.on( 'mouseout', function ( client, args ) {
$(this).popover('hide')
});
});
-
+
+ // Install popover for CocoaPush faving
+
+ $fave_star = $('ol.results img.fave')
+ $fave_star.popover({
+ trigger: "hover",
+ container: "body"
+
+ }).on("click", function(e) {
+ if (e.target.getAttribute('is_faved') === 'true') {
+ unFavePod(e);
+ } else {
+ favePod(e);
+ }
+ });
+
// Install tracking on the allocation selection.
//
allocationSelect.find('li').on('click', function(event) {
@@ -339,7 +393,7 @@ $(window).ready(function() {
trackAllocationSelection(li.find('.text').text(), li.find('.count').text());
// Rest is handled in Picky JS.
});
-
+
// Install tracking on each result link.
//
$('ol.results').find('a').on('click', function(event) {
@@ -366,7 +420,7 @@ $(window).ready(function() {
// simply not show it).
//
groups: [['platform']],
-
+
// This is used for formatting inside the choice groups.
//
// Use %n$s, where n is the position of the category in the key.
@@ -375,7 +429,7 @@ $(window).ready(function() {
choices: {
en: {
'platform': '', // platform is simply not shown.
-
+
'name': 'name',
'author': 'author',
'summary': 'summary',
@@ -447,9 +501,9 @@ $(window).ready(function() {
// }
// }
});
-
+
// Reset the search if it has been cleared and track when it has.
- //
+ //
searchInput.on('input', function(e) {
if ('' == this.value) {
_gaq.push(['_trackEvent', 'clear']);
@@ -468,13 +522,13 @@ $(window).ready(function() {
pickyClient.resend();
selectCheckedPlatform();
});
-
+
// Make all clicks in the search container set focus.
- //
+ //
$('#search_container').on('click', function (e) {
searchInput.focus();
});
-
+
// Keyboard handling.
//
// Currently, we only handle keyboard selecting the first result category set.
@@ -506,7 +560,7 @@ $(window).ready(function() {
window.document.location.href = selected.find('a').first().attr('href');
}
}
-
+
// Install keyboard handling.
//
$('body').keydown(function(event) {
@@ -516,7 +570,7 @@ $(window).ready(function() {
case 40:
selectResult(nextResult)
break;
-
+
// Up
//
case 38:
@@ -531,7 +585,7 @@ $(window).ready(function() {
break;
}
});
-
+
// Initially select the right platform.
//
if (window.initial_query != "") {
View
8 source/push.html.slim
@@ -0,0 +1,8 @@
+---
+title: Push Notifications
+---
+
+p This page is a test for push notifications.
+
+javascript:
+ window.safari.pushNotification.requestPermission('https://localhost:3000/push', 'web.org.cocoapods.push', null, function(permission) { console.log(permission); });
Something went wrong with that request. Please try again.