Filtering Sparkle Release Notes with JavaScript

February 21, 2012

Up ‘til now I’ve been using the description field in my Sparkle appcast to display release notes. It’s easy and I like showing users just the notes that are relevant to the update. The downside to this approach is that if someone doesn’t run the app often they are likely to be a couple releases behind and not get the full picture. This is especially true if I’ve recently released a significant update. Such updates usually have the most important notes and are usually followed pretty quickly by a patch release. Unless the user runs the app before the patch release goes out they’ll miss the good bits.

There are a few ways to deal with this. The simplest is to use the releaseNotesLink field and just show release notes for all versions every time, leaving it up to the user to look at the version numbers and figure out what is relevant. Another option is to append a “new in $(last significant update)” section to the description HTML as Apple does. I thought about going this route, but remembered that in the apps I use sometimes it has been the smallest change that has had the most impact. If it was that One Thing that drove me nuts about an otherwise-great app that’s what I would be looking for every time.

The final approach I’ve seen is to pass the version range as parameters to the feed URL and build a custom appcast on the server. This offers the output I’m looking for but I wondered if I could do the same thing with a static page. With a little Sparkle hack it turned out to be possible and I’m pretty happy with the result.

Filtered Sparkle Release Notes

To build this I took a page with full release note history and wrapped every release in a div with its id set to the version number. These were grouped under a parent element with nothing else in it, then I put the following in a script tag:

function getQueryParameter(name) {
    // From http://james.padolsey.com/javascript/bujs-1-getparameterbyname/
    var match = RegExp("[?&]" + name + "=([^&]*)").exec(window.location.search);
    return match && decodeURIComponent(match[1].replace(/\+/g, " "));
}

function filterVersions() {
    var currentVersion = getQueryParameter("currentVersion");
    var updateVersion = getQueryParameter("updateVersion");
    var updateVersionIndex = undefined;
    var currentVersionIndex = undefined;
    if (!updateVersion) {
        updateVersionIndex = 0;
    }

    if (!currentVersion || currentVersion == updateVersion) {
        return;
    }

    var releases = document.getElementById("releases").children;
    var length = releases.length;
    for (var i=0; i < length; i++) {
        var elem = releases[i];
        if (updateVersionIndex === undefined && elem.id == updateVersion) {
            updateVersionIndex = i;
        }

        if (currentVersionIndex === undefined && elem.id == currentVersion) {
            currentVersionIndex = i;
        }

        if (i >= currentVersionIndex || updateVersionIndex === undefined) {
            releases[i].style.display = "none";
        }
    }
}

addEventListener("DOMContentLoaded", filterVersions);

Assuming my releases are in descending order by version this will hide everything that falls outside the range parameters included in the URL. The next step was to get Sparkle to include those parameters when requesting the release notes. I replaced the description field with a releaseNotesLink pointing to the new page, then used the following tweak in the app:

- (void)updater:(SUUpdater *)updater didFindValidUpdate:(SUAppcastItem *)update {
    NSString *urlString = [[update releaseNotesURL] absoluteString];
    urlString = [urlString stringByAppendingFormat:@"?currentVersion=%@&updateVersion=%@",
                 [[[updater hostBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"],
                 [update displayVersionString]];
    [update setValue:[NSURL URLWithString:urlString] forKey:@"releaseNotesURL"];
}

There isn’t a public API to modify releaseNotesURL but there is a set method implemented so I used setValue:forKey: to invoke it and boom, release notes tailored to the updated version range without anything special running on the server.