Automators Forum Feed Widget

I thought it might be neat to put together a widget in Scriptable to show the latest posts in the forums.

I’m curious on your thoughts

1 Like

I love it (with one caveat), particularly the layout for the headlines / subject lines. Most of the newsfeed-style widgets I’ve seen (including ones I’ve tried to make) end up looking like blocks of text rather than lists of distinct headlines.

The one caveat is that the busy background makes the foreground too hard to read. I wonder if you can fade it even more — a lot more — and still get the Automators feel without competing for the viewer’s attention.

1 Like

Overall, it looks awesome! I agree with @tf2 that the Automators logo background makes it look a little too busy.

A couple of suggestions:

  • A little bit of space between the profile pictures and the post titles would make it look neater.
  • I think the background should be solid darkish blue (204b69), the main color of this forum’s theme. I don’t really need the logo to get the Automators feel. The logo could be something that would only be shown in large widgets, and omitted from small and medium widgets.

Once again, great work!

1 Like

Great suggestions guys!

I have made some further tweaks. I’m wondering about the best way to avoid having the Unicode character issues when presenting the text though.

Let me know your thoughts

@tf2 and @FifiTheBulldog

1 Like

Looks good!

How are you getting the list of posts? Are you scraping the webpage? Using the JSON feed? RSS?

Edit: what supermamon said. The URL for the Scriptable topic is The path to get the array of recent posts is topic_list.topics.

1 Like

If you’re using the API, you should be able to get the title in plain text without html encoding.

1 Like

This is great info, I didn’t realize there was a way to get JSON from an endpoint of the site.

The trouble I see now, is that the array of recent posts doesn’t contain a reference to the thread starter’s profile. I could use the most recent reply user’s info to get their profile image and use that in the widget.

Maybe that would be more representative of how the feed works on the site actually.

Curious to hear thoughts from you guys further

The top-level users property of the JSON is an array of user objects, each of which includes a key called avatar_template. The value of that key is a URL path, and all you need to do is give it the scheme and domain and replace {size} with whatever size you want (32 perhaps?). Then you can load the image from the resulting URL.

Edit: the posters property of a post object contains an array of people who have contributed to the thread. You could filter them by their description: if description contains “Original Poster” then the user is the thread starter. You’d have to filter the master users list by user ID.

1 Like

That makes more sense now. I was wondering why that array of the posters didn’t match the posters array in the HTML that I was seeing when scraping. That being said, I did find that using{username}.json gives the specified user’s JSON data.

Now I’m just left with determining what is better: displaying the avatar of the thread starter or the latest commenter / poster. I personally like the idea of seeing the starter’s photo

Sidebar. I’m curious on your stacks layout actually. More specifically, the vertically centered title that also wraps to the next line if it doesn’t fit one. When I’m trying a similar layout, the title does not wrap even after adding a lineLimit to the WidgetText item.

I was having a hard time with that as well and I happened to look back to my previous Twitter Widget. I didn’t realize it until now actually (it was reminded :laughing:), but setting the size of the sub stack (right in this case, which is inside of stacker) with only the height, allows wrapping of the WidgetText and also centers the text in the stack vertically.

Here is the code if you want to take a look at how I’ve written this. If you see anything strange or optimization opportunities, please do let me know

Seems okay overall. Promise.all is a great use for this but you may want to be mindful of the possible consequence. Since the promises are executed asynchronously, it’s not guaranteed that they would be finished in order. So there’s a chance that it may be adding the topics in the wrong order. You can resolve this by loading all the data first then building the stack after.

Here’s a snippet on how might that go

const postsData = await Promise.all( async (fin) => {
    const url = ...
    const avatar_image = ...
    const title = ...
    return {url, avatar_image, title}

for (post of postsData) {
   // build the stacks

Promise.all always returns the results in the same order of the passed promises. Otherwise it would provide a lot of headache.

Yes the results are in order.
The issue lies when we want to add visual elements in same order as the data.
Each promise is executed in parallel and the visual elements are being added within each promise so it’s possible that they may not be completed in order.

1 Like

Oh, sorry. Should have read the whole post before answering :man_facepalming:

1 Like

Is that still the case when there is an await to load the image for each element of the array?

Yes. await is simply another syntax for calling promises. With regards to this case just returns an array of async functions (aka promises) and does not execute them.

Promise.all() then executes them in parallel to which, you won’t be able to ensure which one finishes first.

This is interesting info for sure. When I was reading up and refreshing my memory on the map functionality, it seemed it should process them all in order but I think that was not running in async.

So what would keep them in the right order utilizing the method you displayed above?

I take back what I said that “map does not execute” the callback function. That didn’t mean what I wanted it to mean.

map does execute the callback, but it calls the callback as a regular function and not with an await. That would mean each call will immediately return without waiting for it to complete.

I guess it would be better illustrated with an example. Let’s ignore Promise.all for the moment and focus on map. Refer to the code below. I threw in a random delay to simulate web calls like image downloads or API calls.

function randomInterval(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min)
function delay(ms) {
  return new Promise( (resolve, reject) => {
    const t = new Timer()
    t.timeInterval = ms
    t.schedule( ()=> {

const arr = [1,2,3,4,5]
const RANDOMIZE_DELAY = true

async function processItem(item) {
  log(`${item} is being processed`)
  const ret = 2*item
  const interval = RANDOMIZE_DELAY ? randomInterval(250,1000) : 250
  await delay(interval)
  log(`${item} completed in ${interval}ms`)
  return ret

const results = processItem )

results to

2021-09-28 00:07:01: 1 is being processed
2021-09-28 00:07:01: 2 is being processed
2021-09-28 00:07:01: 3 is being processed
2021-09-28 00:07:01: 4 is being processed
2021-09-28 00:07:01: 5 is being processed
2021-09-28 00:07:01: [{},{},{},{},{}]
2021-09-28 00:07:01: Promise
2021-09-28 00:07:02: 2 completed in 321ms
2021-09-28 00:07:02: 1 completed in 497ms
2021-09-28 00:07:03: 3 completed in 528ms
2021-09-28 00:07:03: 4 completed in 554ms
2021-09-28 00:07:03: 5 completed in 850ms

Note that map returns immediately after initiating the callbacks. But the callbacks do continue running. Also, since there’s no control how long each callback completes, they may end not in order. In the widget’s case, it just so happened that each callback was done in the same amount of time or close to same.

Now, If we were to enclose Promise.all

const results = await Promise.all( processItem ))
2021-09-28 00:18:04: 1 is being processed
2021-09-28 00:18:04: 2 is being processed
2021-09-28 00:18:04: 3 is being processed
2021-09-28 00:18:04: 4 is being processed
2021-09-28 00:18:04: 5 is being processed
2021-09-28 00:18:04: 4 completed in 433ms
2021-09-28 00:18:04: 1 completed in 562ms
2021-09-28 00:18:04: 3 completed in 648ms
2021-09-28 00:18:04: 2 completed in 691ms
2021-09-28 00:18:04: 5 completed in 740ms
2021-09-28 00:18:04: [2,4,6,8,10]

They still did not finished in order but Promise.all waited for them to complete before doing anything else. Take note of the results [2,4,6,8,10]. These are still in the correct order even though they did not finished in order.

This is what I meant with try pulling all the necessary data first so that you get them in the correct order. Then add the visual elements on a regular loop (for…of).

I hope I explained myself better.


This definitely makes more sense. I implemented this (I think) based on what you explained.

Here it is