Making WordPress.org

Changeset 1105


Ignore:
Timestamp:
01/08/2015 06:29:55 PM (8 years ago)
Author:
iandunn
Message:

Central Theme: Replace sessions on the homepage with latest tweets.

Props mj12982 for the design

Location:
sites/trunk/wordcamp.org/public_html/wp-content/themes/wordcamp-central-2012
Files:
3 added
6 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/wordcamp.org/public_html/wp-content/themes/wordcamp-central-2012/functions.php

    r1085 r1105  
    3636        add_action( 'init', array( __CLASS__, 'process_forms' ) );
    3737        add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
     38        add_action( 'wp_ajax_get_latest_wordcamp_tweets', array( __CLASS__, 'get_latest_tweets' ) );
     39        add_action( 'wp_ajax_nopriv_get_latest_wordcamp_tweets', array( __CLASS__, 'get_latest_tweets' ) );
    3840
    3941        add_filter( 'excerpt_more', array( __CLASS__, 'excerpt_more' ), 11 );
     
    181183    static function enqueue_scripts() {
    182184        wp_enqueue_style( 'central', get_stylesheet_uri(), array(), 3 );
    183         wp_enqueue_script( 'wordcamp-central', get_stylesheet_directory_uri() . '/js/central.js', array(), '20140909', true );
     185        wp_enqueue_script( 'wordcamp-central', get_stylesheet_directory_uri() . '/js/central.js', array( 'jquery', 'underscore' ), 1, true );
     186
     187        wp_localize_script( 'wordcamp-central', 'wordcampCentralOptions', array( 'ajaxURL' => admin_url( 'admin-ajax.php' ) ) );
    184188
    185189        /* We add some JavaScript to pages with the comment form
     
    425429    public static function can_subscribe() {
    426430        return class_exists( 'Jetpack_Subscriptions' ) && is_callable( array( 'Jetpack_Subscriptions', 'subscribe' ) );
     431    }
     432
     433    /**
     434     * Fetch the latest tweets from the @WordCamp account
     435     *
     436     * This is an AJAX callback returning JSON-formatted data.
     437     *
     438     * We're manually expiring/refreshing the transient to ensure that we only ever update it when we have a
     439     * valid response from the API. If there is a problem retrieving new data from the API, then we want to
     440     * continue displaying the valid cached data until we can successfully retrieve new data. The data is still
     441     * stored in a transient instead of an option, though, so that it can be cached in memory.
     442     *
     443     * Under certain unlikely conditions, this could cause an API rate limit violation. If the data is expired
     444     * and we can connect to the API at the network level, but then the request fails at the application level
     445     * (invalid credentials, etc), then we'll be hitting the API every time this function is called. If that
     446     * does ever happen, it could be fixed by setting the timestamp of the last attempt in a transient and only
     447     * issuing another attempt if ~2 minutes have passed.
     448     */
     449    public static function get_latest_tweets() {
     450        $transient_key = 'wcc_latest_tweets';
     451        $tweets        = get_transient( $transient_key );
     452        $expired       = $tweets['last_update'] < strtotime( 'now - 15 minutes' );
     453
     454        if ( ! $tweets || $expired ) {
     455            $response = wp_remote_get(
     456                'https://api.twitter.com/1.1/statuses/user_timeline.json?count=6&trim_user=true&exclude_replies=true&include_rts=false&screen_name=wordcamp',
     457                array(
     458                    'headers' => array( 'Authorization' => 'Bearer ' . TWITTER_BEARER_TOKEN_WORDCAMP_CENTRAL ),
     459                )
     460            );
     461
     462            if ( ! is_wp_error( $response ) ) {
     463                $tweets['tweets'] = json_decode( wp_remote_retrieve_body( $response ) );
     464
     465                /*
     466                 * Remove all but the first 3 tweets
     467                 *
     468                 * The Twitter API includes retweets in the `count` parameter, even if include_rts=false is passed,
     469                 * so we have to request more tweets than we actually want and then cut it down here.
     470                 */
     471                if ( $tweets['tweets'] ) {
     472                    $tweets['tweets']      = array_slice( $tweets['tweets'], 0, 3 );
     473                    $tweets['tweets']      = self::sanitize_format_tweets( $tweets['tweets'] );
     474                    $tweets['last_update'] = time();
     475
     476                    set_transient( $transient_key, $tweets );
     477                }
     478            }
     479        }
     480
     481        wp_send_json_success( $tweets );
     482    }
     483
     484    /**
     485     * Sanitize and format the tweet objects
     486     *
     487     * Whitelist the fields to cut down on how much data we're storing/transmitting, but also to force
     488     * future devs to manually enable/sanitize any new fields that are used, which avoids the risk of
     489     * accidentally using an unsafe value.
     490     *
     491     * @param array $tweets
     492     *
     493     * @return array
     494     */
     495    protected static function sanitize_format_tweets( $tweets ) {
     496        $whitelisted_fields = array( 'id_str' => '', 'text' => '', 'created_at' => '' );
     497
     498        foreach ( $tweets as & $tweet ) {
     499            $tweet           = (object) shortcode_atts( $whitelisted_fields, $tweet );
     500            $tweet->id_str   = sanitize_text_field( $tweet->id_str );
     501            $tweet->text     = wp_kses( $tweet->text, wp_kses_allowed_html( 'data' ), array( 'http', 'https', 'mailto' ) );
     502            $tweet->text     = make_clickable( $tweet->text );
     503            $tweet->text     = self::link_hashtags_and_usernames( $tweet->text );
     504            $tweet->time_ago = human_time_diff( strtotime( $tweet->created_at ) );
     505        }
     506
     507        return $tweets;
     508    }
     509
     510    /**
     511     * Convert usernames and hashtags to links
     512     *
     513     * Based on Tagregator's TGGRSourceTwitter::link_hashtags_and_usernames().
     514     *
     515     * @param string $text
     516     *
     517     * @return string
     518     */
     519    protected static function link_hashtags_and_usernames( $content ) {
     520        $content = preg_replace( '/@(\w+)/',       '<a href="https://twitter.com/\\1"          class="wc-tweets-username">@\\1</a>', $content );
     521        $content = preg_replace( '/(?<!&)#(\w+)/', '<a href="https://twitter.com/search?q=\\1" class="wc-tweets-tag"     >#\\1</a>', $content );
     522
     523        return $content;
    427524    }
    428525
  • sites/trunk/wordcamp.org/public_html/wp-content/themes/wordcamp-central-2012/js/central.js

    r1085 r1105  
    1 /**
    2  * Toggle the navigation menu for small screens.
    3  */
    4 ( function() {
    5     var container, button, menu;
     1var WordCampCentral = ( function( $ ) {
    62
    7     container = document.getElementById( 'access' );
    8     if ( ! container ) {
    9         return;
     3    // templateOptions is copied from Core in order to avoid an extra HTTP request just to get wp.template
     4    var ajaxURL,
     5        templateOptions = {
     6            evaluate:    /<#([\s\S]+?)#>/g,
     7            interpolate: /\{\{\{([\s\S]+?)\}\}\}/g,
     8            escape:      /\{\{([^\}]+?)\}\}(?!\})/g
     9        };
     10
     11    /**
     12     * Initialization that runs as soon as this file has loaded
     13     */
     14    function immediateInit( options ) {
     15        ajaxURL = options.ajaxURL;
     16
     17        toggleNavigation();
     18        populateLatestTweets();
    1019    }
    1120
    12     button = container.getElementsByTagName( 'button' )[0];
    13     if ( 'undefined' === typeof button ) {
    14         return;
     21    /**
     22     * Initialization that runs when the document has fully loaded
     23     */
     24    function documentReadyInit() {
    1525    }
    1626
    17     menu = container.getElementsByTagName( 'ul' )[0];
     27    /**
     28     * Toggle the navigation menu for small screens.
     29     */
     30    function toggleNavigation() {
     31        var container, button, menu;
    1832
    19     // Hide menu toggle button if menu is empty and return early.
    20     if ( 'undefined' === typeof menu ) {
    21         button.style.display = 'none';
    22         return;
     33        container = document.getElementById( 'access' );
     34        if ( ! container ) {
     35            return;
     36        }
     37
     38        button = container.getElementsByTagName( 'button' )[0];
     39        if ( 'undefined' === typeof button ) {
     40            return;
     41        }
     42
     43        menu = container.getElementsByTagName( 'ul' )[0];
     44
     45        // Hide menu toggle button if menu is empty and return early.
     46        if ( 'undefined' === typeof menu ) {
     47            button.style.display = 'none';
     48            return;
     49        }
     50
     51        if ( -1 === menu.className.indexOf( 'nav-menu' ) ) {
     52            menu.className += ' nav-menu';
     53        }
     54
     55        button.onclick = function() {
     56            if ( -1 !== container.className.indexOf( 'toggled' ) ) {
     57                container.className = container.className.replace( ' toggled', '' );
     58            } else {
     59                container.className += ' toggled';
     60            }
     61        };
    2362    }
    2463
    25     if ( -1 === menu.className.indexOf( 'nav-menu' ) ) {
    26         menu.className += ' nav-menu';
     64    /**
     65     * Fetch the latest tweets and inject them into the DOM
     66     */
     67    function populateLatestTweets() {
     68        $.getJSON(
     69            ajaxURL,
     70            { action: 'get_latest_wordcamp_tweets' },
     71            function( response ) {
     72                var index, tweets,
     73                    spinner         = $( '#wc-tweets-spinner' ),
     74                    error           = $( '#wc-tweets-error' ),
     75                    tweetsContainer = $( '#wc-tweets-container' ),
     76                    tweetTemplate   = _.template( $( '#tmpl-wc-tweet' ).html(), null, templateOptions );
     77
     78                // Check for success
     79                if ( response.hasOwnProperty( 'data' ) && response.data.hasOwnProperty( 'tweets' ) ) {
     80                    tweets = response.data.tweets;
     81                } else {
     82                    spinner.addClass(  'hidden' );
     83                    error.removeClass( 'hidden' );
     84                    error.removeAttr(  'hidden' );
     85                    return;
     86                }
     87
     88                // Populate and reveal the container
     89                for ( index in tweets ) {
     90                    if ( tweets.hasOwnProperty( index ) ) {
     91                        tweetsContainer.append( tweetTemplate( { 'tweet': tweets[ index ] } ) );
     92                    }
     93                }
     94
     95                spinner.addClass( 'hidden' );
     96                tweetsContainer.removeClass( 'transparent' );
     97            }
     98        );
    2799    }
    28100
    29     button.onclick = function() {
    30         if ( -1 !== container.className.indexOf( 'toggled' ) ) {
    31             container.className = container.className.replace( ' toggled', '' );
    32         } else {
    33             container.className += ' toggled';
    34         }
     101    return {
     102        immediateInit:     immediateInit,
     103        documentReadyInit: documentReadyInit
    35104    };
     105} )( jQuery );
    36106
    37 } )();
     107WordCampCentral.immediateInit( wordcampCentralOptions );
     108jQuery( document ).ready( WordCampCentral.documentReadyInit );
  • sites/trunk/wordcamp.org/public_html/wp-content/themes/wordcamp-central-2012/style.css

    r852 r1105  
    335335*:first-child+html .group,
    336336*:first-child+html li { min-height: 1px }
     337.clearfix:after {
     338    content: "";
     339    display: table;
     340    clear: both;
     341}
    337342
    338343/* !01d. Buttons */
     
    513518}
    514519
     520.spinner {
     521    background: url( 'images/spinner.gif' ) no-repeat;
     522    background-size: 20px 20px;
     523    display: none;
     524    float: right;
     525    opacity: 0.7;
     526    filter: alpha(opacity=70);
     527    width: 20px;
     528    height: 20px;
     529    margin: 2px 5px 0;
     530}
     531
     532.spinner-visible {
     533    display: block;
     534}
     535
     536.hidden {
     537    display: none;
     538}
     539
     540.transparent {
     541    opacity: 0;
     542}
     543
    515544/* !01f. Header & Footer */
    516545/* ------------------------------------- */
     
    10211050}
    10221051#wc-content-blocks .wc-news h3 { background-position: 0 -71px }
     1052#wc-content-blocks .wc-tweets h3 {
     1053    background-position: 0 -658px;
     1054    padding-left: 40px;
     1055    margin-bottom: 9px;
     1056}
    10231057#wc-content-blocks .wc-sessions h3 {
    10241058    background-position: -7px -150px;
     
    11191153}
    11201154
     1155
     1156/* !Latest Tweets */
     1157#wc-tweets-spinner {
     1158    float: none;
     1159    margin: 0 auto;
     1160}
     1161
     1162#wc-tweets-container {
     1163    transition: all 1.25s ease;
     1164}
     1165#wc-tweets-container li {
     1166    position: relative;
     1167    border-top: 1px solid #EEEEEE;
     1168    padding: 1em 0;
     1169}
     1170
     1171.wc-tweet-timestamp {
     1172    position: absolute;
     1173    bottom: 1em;    /* matches #wc-tweets-container li margin-bottom */
     1174    right: 0;
     1175    margin-bottom: 0;
     1176}
     1177
     1178#wc-tweets-container li .wc-tweet-actions li {
     1179    float: left;
     1180    margin-right: 12px;
     1181    padding-top: 8px;
     1182    padding-bottom: 0;
     1183    border-top: none;
     1184}
     1185.wc-tweet-actions li a,
     1186.wc-tweet-timestamp a {
     1187    font-weight: normal;
     1188    color: #b2b2b2;
     1189}
     1190.wc-tweet-actions li a:hover,
     1191.wc-tweet-timestamp a:hover {
     1192    color: #666666;
     1193}
     1194.wc-tweet-action-icon {
     1195    position: relative;
     1196    top: 3px;
     1197    display: inline-block;
     1198    width: 16px;
     1199    height: 16px;
     1200    background-image: url( 'images/twitter-actions.png' );
     1201    background-repeat: no-repeat;
     1202}
     1203.wc-tweet-action-reply .wc-tweet-action-icon {
     1204    background-position: 0 0;
     1205}
     1206.wc-tweet-action-reply:hover .wc-tweet-action-icon {
     1207    background-position: -16px 0;
     1208}
     1209.wc-tweet-action-retweet .wc-tweet-action-icon {
     1210    width: 19px;
     1211    background-position: -80px 0;
     1212}
     1213.wc-tweet-action-retweet:hover .wc-tweet-action-icon {
     1214    background-position: -99px 0;
     1215}
     1216.wc-tweet-action-favorite .wc-tweet-action-icon {
     1217    top: 2px;
     1218    background-position: -32px 0;
     1219}
     1220.wc-tweet-action-favorite:hover .wc-tweet-action-icon {
     1221    background-position: -48px 0;
     1222}
    11211223
    11221224/* !WordCamp Sessions */
     
    26512753/* ============================================= */
    26522754@media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi) {
     2755    .spinner {
     2756        background-image: url( 'images/spinner-2x.gif' );
     2757    }
    26532758    #access .sub-menu, #header {
    26542759        background: #e0f5fa url('images/header-bg-2x.png');
     
    26842789    #wc-content-blocks .wc-news h3 {
    26852790        background-position: 0 -71px;
     2791    }
     2792    #wc-content-blocks .wc-tweets h3 {
     2793        background-position: 0 -658px;
    26862794    }
    26872795    #wc-content-blocks .wc-sessions h3 {
     
    29043012        margin-right: 0;
    29053013    }
     3014
     3015    .wc-tweet-timestamp {
     3016        position: relative;
     3017        bottom: auto;
     3018        right: auto;
     3019        margin-top: 10px;
     3020    }
     3021    .wc-tweet-timestamp a::after {
     3022        content: " ago"
     3023    }
     3024    #wc-tweets-container li .wc-tweet-actions li {
     3025        padding-top: 0;
     3026    }
    29063027}
    29073028
  • sites/trunk/wordcamp.org/public_html/wp-content/themes/wordcamp-central-2012/template-home.php

    r842 r1105  
    8989        </div><!-- .wc-news -->
    9090
    91         <div class="wc-sessions last">
    92             <h3>WordCamp <strong>Sessions</strong></h3>
     91        <div class="wc-tweets last">
     92            <h3><strong>Latest Tweets</strong></h3>
    9393
    94             <?php $sessions = WordCamp_Central_Theme::get_sessions(); ?>
    95             <?php if ( ! empty( $sessions ) ) : ?>
    96             <ul>
    97                 <?php foreach( $sessions as $session ) : ?>
     94            <div id="wc-tweets-spinner" class="spinner spinner-visible"></div>
     95            <ul id="wc-tweets-container" class="transparent"></ul>
     96
     97            <p id="wc-tweets-error" class="hidden" hidden>
     98                Tweets from <a href="https://twitter.com/wordcamp">@WordCamp</a> are currently unavailable.
     99            </p>
     100
     101            <a href="https://twitter.com/wordcamp" class="more">Follow @WordCamp on Twitter &rarr;</a>
     102
     103            <script id="tmpl-wc-tweet" type="text/html">
    98104                <li>
    99                     <?php echo $session['wordcamp_thumb']; ?>
    100                     <a href="<?php echo esc_url( $session['permalink'] ); ?>" class="wc-session-name"><?php echo esc_html( $session['name'] ); ?></a>
    101                     <span class="wc-session-speakers">by <?php echo esc_html( $session['speakers'] ); ?></span>
    102                     <a href="<?php echo esc_url( $session['wordcamp_permalink'] ); ?>" class="wc-session-wordcamp"><?php echo esc_html( $session['wordcamp_title'] ); ?></a>
     105                    <div class="wc-tweet-content">{{{tweet.text}}}</div>
     106
     107                    <p class="wc-tweet-timestamp">
     108                        <a href="https://twitter.com/wordcamp/status/{{tweet.id_str}}">{{tweet.time_ago}}</a>
     109                    </p>
     110
     111                    <ul class="wc-tweet-actions clearfix">
     112                        <li class="wc-tweet-action-reply">
     113                            <a href="https://twitter.com/intent/tweet?in_reply_to={{tweet.id_str}}">
     114                                <span class="wc-tweet-action-icon"></span>
     115                                Reply
     116                            </a>
     117                        </li>
     118
     119                        <li class="wc-tweet-action-retweet">
     120                            <a href="https://twitter.com/intent/retweet?tweet_id={{tweet.id_str}}">
     121                                <span class="wc-tweet-action-icon"></span>
     122                                Retweet
     123                            </a>
     124                        </li>
     125
     126                        <li class="wc-tweet-action-favorite">
     127                            <a href="https://twitter.com/intent/favorite?tweet_id={{tweet.id_str}}">
     128                                <span class="wc-tweet-action-icon"></span>
     129                                Favorite
     130                            </a>
     131                        </li>
     132                    </ul>
    103133                </li>
    104                 <?php endforeach; ?>
    105             </ul>
    106             <!--<a href="#" class="more">More Speakers &rarr;</a>-->
    107             <?php else : // ! empty( $sessions ) ?>
    108             <p>We're sorry, the speakers list is temporarily unavailable.</p>
    109             <?php endif; ?>
    110         </div>  <!-- .wc-speakers -->
     134            </script>
     135        </div>  <!-- .wc-tweets -->
    111136
    112137    </div> <!-- #wc-content-blocks -->
Note: See TracChangeset for help on using the changeset viewer.