diff --git a/Development Assets/DevelopmentAssets.xcassets/Contents.json b/Development Assets/DevelopmentAssets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Development Assets/DevelopmentAssets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Development Assets/DevelopmentAssets.xcassets/timelineJSON.dataset/Contents.json b/Development Assets/DevelopmentAssets.xcassets/timelineJSON.dataset/Contents.json new file mode 100644 index 0000000..7e6a022 --- /dev/null +++ b/Development Assets/DevelopmentAssets.xcassets/timelineJSON.dataset/Contents.json @@ -0,0 +1,13 @@ +{ + "data" : [ + { + "filename" : "timeline.json", + "idiom" : "universal", + "universal-type-identifier" : "public.json" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Development Assets/DevelopmentAssets.xcassets/timelineJSON.dataset/timeline.json b/Development Assets/DevelopmentAssets.xcassets/timelineJSON.dataset/timeline.json new file mode 100644 index 0000000..ed07da0 --- /dev/null +++ b/Development Assets/DevelopmentAssets.xcassets/timelineJSON.dataset/timeline.json @@ -0,0 +1,1581 @@ +[ + { + "id": "104708437047868802", + "created_at": "2020-08-18T04:12:25.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://squeet.me/objects/962c3e10-105f-3b55-282d-896121106910", + "url": "https://squeet.me/display/962c3e10-105f-3b55-282d-896121106910", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 1, + "content": " \u2672 @UCmyGZ0689ODyReHw3rsKLtQ@invidio.us:

- #ThisWeekInLinux -

- #TWIL #Week #Linux -

- This Week in Linux 113: Red Hat + Flatpak, KDE Neon, Darktable, RetroArch, HBO Max Drops Linux -

- #bot -

\n
This Week in Linux 113: Red Hat + Flatpak, KDE Neon, Darktable, RetroArch, HBO Max Drops Linux
", + "reblog": null, + "account": { + "id": "957542", + "username": "rixty_dixet", + "acct": "rixty_dixet@squeet.me", + "display_name": "Rixty Dixet", + "locked": true, + "bot": false, + "discoverable": false, + "group": false, + "created_at": "2019-11-02T13:40:24.603Z", + "note": "Co-Admin of #UnitooLiveRadio - https://www.unitoo.it/progetti/radio/ <br> --- <br> If you want you can support the UnitooWebRadio project through ko-fi, buymeacofee, paypal, ethereum or liberapay. You can find everything at this link: https://liberapay.com/UnitooWebRadio", + "url": "https://squeet.me/profile/rixty_dixet", + "avatar": "https://files.mastodon.social/cache/accounts/avatars/000/957/542/original/812b529f4503da60.jpg", + "avatar_static": "https://files.mastodon.social/cache/accounts/avatars/000/957/542/original/812b529f4503da60.jpg", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 34, + "following_count": 43, + "statuses_count": 32084, + "last_status_at": "2020-08-18", + "emojis": [], + "fields": [] + }, + "media_attachments": [ + { + "id": "104708436932084577", + "type": "image", + "url": "https://files.mastodon.social/cache/media_attachments/files/104/708/436/932/084/577/original/81cb5107e7ffcbfa.jpg", + "preview_url": "https://files.mastodon.social/cache/media_attachments/files/104/708/436/932/084/577/small/81cb5107e7ffcbfa.jpg", + "remote_url": "https://tuxdigital.com/wp-content/uploads/2018/04/tuxdigital-shows-banner-this-week-in-linux.jpg", + "preview_remote_url": null, + "text_url": null, + "meta": { + "original": { + "width": 586, + "height": 391, + "size": "586x391", + "aspect": 1.4987212276214834 + }, + "small": { + "width": 490, + "height": 327, + "size": "490x327", + "aspect": 1.4984709480122325 + } + }, + "description": null, + "blurhash": "UIFi+K%L4pNG9Ht7xaR*0LRjxaj[-:RkNGoL" + } + ], + "mentions": [], + "tags": [ + { + "name": "twil", + "url": "https://mastodon.social/tags/twil" + }, + { + "name": "thisweekinlinux", + "url": "https://mastodon.social/tags/thisweekinlinux" + }, + { + "name": "week", + "url": "https://mastodon.social/tags/week" + }, + { + "name": "bot", + "url": "https://mastodon.social/tags/bot" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://invidio.us/watch?v=kBYaY_1BYRA&", + "title": "This Week in Linux 113: Red Hat + Flatpak, KDE Neon, Darktable, RetroArch, HBO Max Drops Linux", + "description": "This Week in Linux is a Proud Member of the Destination Linux Network! https://destinationlinux.network On this episode of This Week in Linux, we've got a lo...", + "type": "video", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 1280, + "height": 720, + "image": "https://files.mastodon.social/cache/preview_cards/images/023/015/754/original/7f8d126bfbdf353c.jpg", + "embed_url": "", + "blurhash": "UDFGCDMv9G?H01IUxtax4.^*jYoJ019F%1ay" + }, + "poll": null + }, + { + "id": "104708247692755991", + "created_at": "2020-08-18T03:24:24.691Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://mastodon.social/users/YesIKnowIT/statuses/104708247692755991", + "url": "https://mastodon.social/@YesIKnowIT/104708247692755991", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

Put emphasis around the Beattles
sed -E 's!(John|Paul|George|Ringo)!<em>&</em>!g' file

#Sed #Linux
from https://www.yesik.it/EP08

", + "reblog": null, + "application": { + "name": "Yes I Know IT", + "website": "https://yesik.it" + }, + "account": { + "id": "25733", + "username": "YesIKnowIT", + "acct": "YesIKnowIT", + "display_name": "Yes, I Know IT ! \ud83c\udf93", + "locked": false, + "bot": false, + "discoverable": null, + "group": false, + "created_at": "2017-03-24T16:41:08.487Z", + "note": "

I'm Sylvain Leroux from France. Use your favorite search engine to learn more about me if you want!

", + "url": "https://mastodon.social/@YesIKnowIT", + "avatar": "https://files.mastodon.social/accounts/avatars/000/025/733/original/21b3a5e955183b9a.jpg", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/025/733/original/21b3a5e955183b9a.jpg", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 390, + "following_count": 6, + "statuses_count": 2100, + "last_status_at": "2020-08-18", + "emojis": [], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + }, + { + "name": "sed", + "url": "https://mastodon.social/tags/sed" + } + ], + "emojis": [], + "card": null, + "poll": null + }, + { + "id": "104707921275930703", + "created_at": "2020-08-18T02:01:20.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://pokemon.men/users/archlinux/statuses/104707921078691325", + "url": "https://pokemon.men/@archlinux/104707921078691325", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

#ArchLinux Audio: no devices no applications, pa_pid_file_create() failed https://bbs.archlinux.org/viewtopic.php?id=258269&action=new #Linux

", + "reblog": null, + "account": { + "id": "803769", + "username": "archlinux", + "acct": "archlinux@pokemon.men", + "display_name": "ArchLinux :archlinux:", + "locked": false, + "bot": true, + "discoverable": true, + "group": false, + "created_at": "2019-05-06T09:29:27.890Z", + "note": "

#ArchLinux #Linux

", + "url": "https://pokemon.men/@archlinux", + "avatar": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 551, + "following_count": 3, + "statuses_count": 36276, + "last_status_at": "2020-08-18", + "emojis": [ + { + "shortcode": "archlinux", + "url": "https://files.mastodon.social/custom_emojis/images/000/098/720/original/df864361dfd59662.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/098/720/static/df864361dfd59662.png", + "visible_in_picker": true + } + ], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "archlinux", + "url": "https://mastodon.social/tags/archlinux" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://bbs.archlinux.org/viewtopic.php?id=258269&action=new", + "title": "Audio: no devices no applications, pa_pid_file_create() failed / Applications & Desktop Environments / Arch Linux Forums", + "description": "", + "type": "link", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 0, + "height": 0, + "image": null, + "embed_url": "", + "blurhash": null + }, + "poll": null + }, + { + "id": "104707803181059016", + "created_at": "2020-08-18T01:31:18.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://pokemon.men/users/archlinux/statuses/104707802973756157", + "url": "https://pokemon.men/@archlinux/104707802973756157", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

#ArchLinux One, System-wide Resolution https://bbs.archlinux.org/viewtopic.php?id=258268&action=new #Linux

", + "reblog": null, + "account": { + "id": "803769", + "username": "archlinux", + "acct": "archlinux@pokemon.men", + "display_name": "ArchLinux :archlinux:", + "locked": false, + "bot": true, + "discoverable": true, + "group": false, + "created_at": "2019-05-06T09:29:27.890Z", + "note": "

#ArchLinux #Linux

", + "url": "https://pokemon.men/@archlinux", + "avatar": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 551, + "following_count": 3, + "statuses_count": 36276, + "last_status_at": "2020-08-18", + "emojis": [ + { + "shortcode": "archlinux", + "url": "https://files.mastodon.social/custom_emojis/images/000/098/720/original/df864361dfd59662.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/098/720/static/df864361dfd59662.png", + "visible_in_picker": true + } + ], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "archlinux", + "url": "https://mastodon.social/tags/archlinux" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://bbs.archlinux.org/viewtopic.php?id=258268&action=new", + "title": "One, System-wide Resolution / Laptop Issues / Arch Linux Forums", + "description": "", + "type": "link", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 0, + "height": 0, + "image": null, + "embed_url": "", + "blurhash": null + }, + "poll": null + }, + { + "id": "104707685161810603", + "created_at": "2020-08-18T01:01:19.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://pokemon.men/users/archlinux/statuses/104707685018408709", + "url": "https://pokemon.men/@archlinux/104707685018408709", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

#ArchLinux [SOLVED] Make some XFCE panel plugins icons smaller https://bbs.archlinux.org/viewtopic.php?id=222292&action=new #Linux

", + "reblog": null, + "account": { + "id": "803769", + "username": "archlinux", + "acct": "archlinux@pokemon.men", + "display_name": "ArchLinux :archlinux:", + "locked": false, + "bot": true, + "discoverable": true, + "group": false, + "created_at": "2019-05-06T09:29:27.890Z", + "note": "

#ArchLinux #Linux

", + "url": "https://pokemon.men/@archlinux", + "avatar": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 551, + "following_count": 3, + "statuses_count": 36276, + "last_status_at": "2020-08-18", + "emojis": [ + { + "shortcode": "archlinux", + "url": "https://files.mastodon.social/custom_emojis/images/000/098/720/original/df864361dfd59662.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/098/720/static/df864361dfd59662.png", + "visible_in_picker": true + } + ], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "archlinux", + "url": "https://mastodon.social/tags/archlinux" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://bbs.archlinux.org/viewtopic.php?id=222292&action=new", + "title": "[SOLVED] Make some XFCE panel plugins icons smaller / Newbie Corner / Arch Linux Forums", + "description": "", + "type": "link", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 0, + "height": 0, + "image": null, + "embed_url": "", + "blurhash": null + }, + "poll": null + }, + { + "id": "104707667724226355", + "created_at": "2020-08-18T00:56:55.076Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://mastodon.social/users/Decentralize_today/statuses/104707667724226355", + "url": "https://mastodon.social/@Decentralize_today/104707667724226355", + "replies_count": 0, + "reblogs_count": 3, + "favourites_count": 1, + "content": "

Facebook to take board seat at Linux Foundation after signing as Platinum member

#deletefacebook #deletefacebooknow #linux

http://web.archive.org/web/20200818005521/https://www.theregister.com/2020/08/17/facebook_platinum_member_linux_foundation/

", + "reblog": null, + "application": { + "name": "Whalebird", + "website": "https://whalebird.org" + }, + "account": { + "id": "772454", + "username": "Decentralize_today", + "acct": "Decentralize_today", + "display_name": "Decentralize.Today", + "locked": false, + "bot": false, + "discoverable": true, + "group": false, + "created_at": "2019-03-23T02:09:11.268Z", + "note": "

The future won't be centralized

", + "url": "https://mastodon.social/@Decentralize_today", + "avatar": "https://files.mastodon.social/accounts/avatars/000/772/454/original/08c8790703a92265.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/772/454/original/08c8790703a92265.png", + "header": "https://files.mastodon.social/accounts/headers/000/772/454/original/1194Decentralize.Today-Logo-Tagline-V3-01-DarkBG.png", + "header_static": "https://files.mastodon.social/accounts/headers/000/772/454/original/1194Decentralize.Today-Logo-Tagline-V3-01-DarkBG.png", + "followers_count": 883, + "following_count": 218, + "statuses_count": 2456, + "last_status_at": "2020-08-18", + "emojis": [], + "fields": [ + { + "name": "www", + "value": "https://decentralize.today", + "verified_at": null + } + ] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + }, + { + "name": "deletefacebooknow", + "url": "https://mastodon.social/tags/deletefacebooknow" + }, + { + "name": "deletefacebook", + "url": "https://mastodon.social/tags/deletefacebook" + } + ], + "emojis": [], + "card": { + "url": "http://web.archive.org/web/20200818005521/https://www.theregister.com/2020/08/17/facebook_platinum_member_linux_foundation/", + "title": "Facebook to take board seat at Linux Foundation after signing as Platinum member", + "description": "Already a big mover in Foundation projects like Presto, GraphQL, Osquery and ONNX, so why not go all-in?", + "type": "link", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 400, + "height": 267, + "image": "https://files.mastodon.social/cache/preview_cards/images/023/011/346/original/5337fcab0b8f989c.jpg", + "embed_url": "", + "blurhash": "USJaAy~W8^DiMcD*S%tRvgM{tlxvT0fjxWV[" + }, + "poll": null + }, + { + "id": "104707623619356492", + "created_at": "2020-08-18T00:45:39.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://fosstodon.org/users/nixchick/statuses/104707623442798384", + "url": "https://fosstodon.org/@nixchick/104707623442798384", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

Matthew Arnold: Why I switched to #fedora #linux

https://fedoramagazine.org/matthew-arnold-why-i-switched-to-fedora/

", + "reblog": null, + "account": { + "id": "1035247", + "username": "nixchick", + "acct": "nixchick@fosstodon.org", + "display_name": "nixchick", + "locked": false, + "bot": false, + "discoverable": true, + "group": false, + "created_at": "2019-12-19T02:51:44.093Z", + "note": "

An unhealthy addiction to books,tech, open-source, and coffee.

", + "url": "https://fosstodon.org/@nixchick", + "avatar": "https://files.mastodon.social/cache/accounts/avatars/001/035/247/original/673e80483a6117b8.jpeg", + "avatar_static": "https://files.mastodon.social/cache/accounts/avatars/001/035/247/original/673e80483a6117b8.jpeg", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 140, + "following_count": 64, + "statuses_count": 502, + "last_status_at": "2020-08-18", + "emojis": [], + "fields": [ + { + "name": "Age", + "value": "Old enough to know better.", + "verified_at": null + }, + { + "name": "Location", + "value": "The Midwest, USA", + "verified_at": null + }, + { + "name": "AKA", + "value": "lstench", + "verified_at": null + } + ] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "fedora", + "url": "https://mastodon.social/tags/fedora" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://fedoramagazine.org/matthew-arnold-why-i-switched-to-fedora/", + "title": "Matthew Arnold: Why I switched to Fedora - Fedora Magazine", + "description": "To a veteran user of other distributions, Fedora can be a challenge. If you are switching to Fedora, here are some observations and tips to get you started.", + "type": "link", + "author_name": "marnold512", + "author_url": "https://fedoramagazine.org/author/marnold512/", + "provider_name": "Fedora Magazine", + "provider_url": "https://fedoramagazine.org", + "html": "", + "width": 400, + "height": 169, + "image": "https://files.mastodon.social/cache/preview_cards/images/022/566/822/original/795a2e30937d9250.jpg", + "embed_url": "", + "blurhash": "U542Pij[DzfR-?j[RhfQM]j[%OfPDzayxxfQ" + }, + "poll": null + }, + { + "id": "104707567635429827", + "created_at": "2020-08-18T00:31:18.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://pokemon.men/users/archlinux/statuses/104707567036696269", + "url": "https://pokemon.men/@archlinux/104707567036696269", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

#ArchLinux BSPWM multi-monitor setup https://bbs.archlinux.org/viewtopic.php?id=258267&action=new #Linux

", + "reblog": null, + "account": { + "id": "803769", + "username": "archlinux", + "acct": "archlinux@pokemon.men", + "display_name": "ArchLinux :archlinux:", + "locked": false, + "bot": true, + "discoverable": true, + "group": false, + "created_at": "2019-05-06T09:29:27.890Z", + "note": "

#ArchLinux #Linux

", + "url": "https://pokemon.men/@archlinux", + "avatar": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 551, + "following_count": 3, + "statuses_count": 36276, + "last_status_at": "2020-08-18", + "emojis": [ + { + "shortcode": "archlinux", + "url": "https://files.mastodon.social/custom_emojis/images/000/098/720/original/df864361dfd59662.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/098/720/static/df864361dfd59662.png", + "visible_in_picker": true + } + ], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "archlinux", + "url": "https://mastodon.social/tags/archlinux" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://bbs.archlinux.org/viewtopic.php?id=258267&action=new", + "title": "BSPWM multi-monitor setup / Applications & Desktop Environments / Arch Linux Forums", + "description": "", + "type": "link", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 0, + "height": 0, + "image": null, + "embed_url": "", + "blurhash": null + }, + "poll": null + }, + { + "id": "104707450535913067", + "created_at": "2020-08-18T00:01:21.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://pokemon.men/users/archlinux/statuses/104707449255979646", + "url": "https://pokemon.men/@archlinux/104707449255979646", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

#ArchLinux Darktable 3.2.1 arch package is almost 100% slower than flatpak ver! https://bbs.archlinux.org/viewtopic.php?id=258266&action=new #Linux

", + "reblog": null, + "account": { + "id": "803769", + "username": "archlinux", + "acct": "archlinux@pokemon.men", + "display_name": "ArchLinux :archlinux:", + "locked": false, + "bot": true, + "discoverable": true, + "group": false, + "created_at": "2019-05-06T09:29:27.890Z", + "note": "

#ArchLinux #Linux

", + "url": "https://pokemon.men/@archlinux", + "avatar": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 551, + "following_count": 3, + "statuses_count": 36276, + "last_status_at": "2020-08-18", + "emojis": [ + { + "shortcode": "archlinux", + "url": "https://files.mastodon.social/custom_emojis/images/000/098/720/original/df864361dfd59662.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/098/720/static/df864361dfd59662.png", + "visible_in_picker": true + } + ], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "archlinux", + "url": "https://mastodon.social/tags/archlinux" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://bbs.archlinux.org/viewtopic.php?id=258266&action=new", + "title": "Darktable 3.2.1 arch package is almost 100% slower than flatpak ver! / Applications & Desktop Environments / Arch Linux Forums", + "description": "", + "type": "link", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 0, + "height": 0, + "image": null, + "embed_url": "", + "blurhash": null + }, + "poll": null + }, + { + "id": "104707450254764172", + "created_at": "2020-08-18T00:01:21.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "zhCn", + "uri": "https://pokemon.men/users/archlinux/statuses/104707449225276595", + "url": "https://pokemon.men/@archlinux/104707449225276595", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

#ArchLinux VFIO @boot -> sh vfio-pci-override.sh: permission denied https://bbs.archlinux.org/viewtopic.php?id=255483&action=new #Linux

", + "reblog": null, + "account": { + "id": "803769", + "username": "archlinux", + "acct": "archlinux@pokemon.men", + "display_name": "ArchLinux :archlinux:", + "locked": false, + "bot": true, + "discoverable": true, + "group": false, + "created_at": "2019-05-06T09:29:27.890Z", + "note": "

#ArchLinux #Linux

", + "url": "https://pokemon.men/@archlinux", + "avatar": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 551, + "following_count": 3, + "statuses_count": 36276, + "last_status_at": "2020-08-18", + "emojis": [ + { + "shortcode": "archlinux", + "url": "https://files.mastodon.social/custom_emojis/images/000/098/720/original/df864361dfd59662.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/098/720/static/df864361dfd59662.png", + "visible_in_picker": true + } + ], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "archlinux", + "url": "https://mastodon.social/tags/archlinux" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://bbs.archlinux.org/viewtopic.php?id=255483&action=new", + "title": "VFIO @boot -> sh vfio-pci-override.sh: permission denied / Kernel & Hardware / Arch Linux Forums", + "description": "", + "type": "link", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 0, + "height": 0, + "image": null, + "embed_url": "", + "blurhash": null + }, + "poll": null + }, + { + "id": "104707449889400649", + "created_at": "2020-08-18T00:01:20.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://pokemon.men/users/archlinux/statuses/104707449199508042", + "url": "https://pokemon.men/@archlinux/104707449199508042", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

#ArchLinux Mouse Input stuttering on XPS15 9500 https://bbs.archlinux.org/viewtopic.php?id=257694&action=new #Linux

", + "reblog": null, + "account": { + "id": "803769", + "username": "archlinux", + "acct": "archlinux@pokemon.men", + "display_name": "ArchLinux :archlinux:", + "locked": false, + "bot": true, + "discoverable": true, + "group": false, + "created_at": "2019-05-06T09:29:27.890Z", + "note": "

#ArchLinux #Linux

", + "url": "https://pokemon.men/@archlinux", + "avatar": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 551, + "following_count": 3, + "statuses_count": 36276, + "last_status_at": "2020-08-18", + "emojis": [ + { + "shortcode": "archlinux", + "url": "https://files.mastodon.social/custom_emojis/images/000/098/720/original/df864361dfd59662.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/098/720/static/df864361dfd59662.png", + "visible_in_picker": true + } + ], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "archlinux", + "url": "https://mastodon.social/tags/archlinux" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://bbs.archlinux.org/viewtopic.php?id=257694&action=new", + "title": "Mouse Input stuttering on XPS15 9500 / Laptop Issues / Arch Linux Forums", + "description": "", + "type": "link", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 0, + "height": 0, + "image": null, + "embed_url": "", + "blurhash": null + }, + "poll": null + }, + { + "id": "104707336284420320", + "created_at": "2020-08-17T23:32:37.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://mastodon.technology/users/category/statuses/104707336282287672", + "url": "https://mastodon.technology/@category/104707336282287672", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

#Linux folks, any recommendations for a minimal web browser that can block JavaScript selectively by domain?

I really like nyxt (mostly because of the Emacs styling), but the noscript-mode is either all scripts run or all are blocked - at that point I might as well use lynx!!!

", + "reblog": null, + "account": { + "id": "1257666", + "username": "category", + "acct": "category@mastodon.technology", + "display_name": "Category", + "locked": false, + "bot": false, + "discoverable": false, + "group": false, + "created_at": "2020-08-07T08:33:31.598Z", + "note": "

An unapolegitic nerd, whose beard is turning more grey by the day. Loves Linux, MIDI, Vidya and skateboards. And my cat. And now Emacs.

", + "url": "https://mastodon.technology/@category", + "avatar": "https://files.mastodon.social/cache/accounts/avatars/001/257/666/original/6fa91e2e52aba475.png", + "avatar_static": "https://files.mastodon.social/cache/accounts/avatars/001/257/666/original/6fa91e2e52aba475.png", + "header": "https://files.mastodon.social/cache/accounts/headers/001/257/666/original/527785b22c9ccbc8.png", + "header_static": "https://files.mastodon.social/cache/accounts/headers/001/257/666/original/527785b22c9ccbc8.png", + "followers_count": 3, + "following_count": 12, + "statuses_count": 88, + "last_status_at": "2020-08-17", + "emojis": [], + "fields": [ + { + "name": "Location", + "value": "South Coast, UK", + "verified_at": null + }, + { + "name": "Distro", + "value": "Debian/Raspbian", + "verified_at": null + }, + { + "name": "Window Manager", + "value": "i3", + "verified_at": null + } + ] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": null, + "poll": null + }, + { + "id": "104707331658667403", + "created_at": "2020-08-17T23:31:18.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://pokemon.men/users/archlinux/statuses/104707331098420546", + "url": "https://pokemon.men/@archlinux/104707331098420546", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

#ArchLinuxPackageUpdate iana-etc 20200812-1 any https://www.archlinux.org/packages/core/any/iana-etc/ #ArchLinux #Linux

", + "reblog": null, + "account": { + "id": "803769", + "username": "archlinux", + "acct": "archlinux@pokemon.men", + "display_name": "ArchLinux :archlinux:", + "locked": false, + "bot": true, + "discoverable": true, + "group": false, + "created_at": "2019-05-06T09:29:27.890Z", + "note": "

#ArchLinux #Linux

", + "url": "https://pokemon.men/@archlinux", + "avatar": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 551, + "following_count": 3, + "statuses_count": 36276, + "last_status_at": "2020-08-18", + "emojis": [ + { + "shortcode": "archlinux", + "url": "https://files.mastodon.social/custom_emojis/images/000/098/720/original/df864361dfd59662.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/098/720/static/df864361dfd59662.png", + "visible_in_picker": true + } + ], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "archlinuxpackageupdate", + "url": "https://mastodon.social/tags/archlinuxpackageupdate" + }, + { + "name": "archlinux", + "url": "https://mastodon.social/tags/archlinux" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://www.archlinux.org/packages/core/any/iana-etc/", + "title": "Arch Linux - iana-etc 20200812-1 (any)", + "description": "", + "type": "link", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 0, + "height": 0, + "image": null, + "embed_url": "", + "blurhash": null + }, + "poll": null + }, + { + "id": "104707331149219204", + "created_at": "2020-08-17T23:31:16.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "zhCn", + "uri": "https://pokemon.men/users/archlinux/statuses/104707330980332176", + "url": "https://pokemon.men/@archlinux/104707330980332176", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

#ArchLinux PRIME Synchronization configuration not present https://bbs.archlinux.org/viewtopic.php?id=258265&action=new #Linux

", + "reblog": null, + "account": { + "id": "803769", + "username": "archlinux", + "acct": "archlinux@pokemon.men", + "display_name": "ArchLinux :archlinux:", + "locked": false, + "bot": true, + "discoverable": true, + "group": false, + "created_at": "2019-05-06T09:29:27.890Z", + "note": "

#ArchLinux #Linux

", + "url": "https://pokemon.men/@archlinux", + "avatar": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 551, + "following_count": 3, + "statuses_count": 36276, + "last_status_at": "2020-08-18", + "emojis": [ + { + "shortcode": "archlinux", + "url": "https://files.mastodon.social/custom_emojis/images/000/098/720/original/df864361dfd59662.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/098/720/static/df864361dfd59662.png", + "visible_in_picker": true + } + ], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "archlinux", + "url": "https://mastodon.social/tags/archlinux" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://bbs.archlinux.org/viewtopic.php?id=258265&action=new", + "title": "PRIME Synchronization configuration not present / Laptop Issues / Arch Linux Forums", + "description": "", + "type": "link", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 0, + "height": 0, + "image": null, + "embed_url": "", + "blurhash": null + }, + "poll": null + }, + { + "id": "104707213355742819", + "created_at": "2020-08-17T23:01:18.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://pokemon.men/users/archlinux/statuses/104707213146314212", + "url": "https://pokemon.men/@archlinux/104707213146314212", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

#ArchLinuxPackageUpdate firefox-tridactyl 1.20.1-1 any https://www.archlinux.org/packages/community/any/firefox-tridactyl/ #ArchLinux #Linux

", + "reblog": null, + "account": { + "id": "803769", + "username": "archlinux", + "acct": "archlinux@pokemon.men", + "display_name": "ArchLinux :archlinux:", + "locked": false, + "bot": true, + "discoverable": true, + "group": false, + "created_at": "2019-05-06T09:29:27.890Z", + "note": "

#ArchLinux #Linux

", + "url": "https://pokemon.men/@archlinux", + "avatar": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 551, + "following_count": 3, + "statuses_count": 36276, + "last_status_at": "2020-08-18", + "emojis": [ + { + "shortcode": "archlinux", + "url": "https://files.mastodon.social/custom_emojis/images/000/098/720/original/df864361dfd59662.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/098/720/static/df864361dfd59662.png", + "visible_in_picker": true + } + ], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "archlinuxpackageupdate", + "url": "https://mastodon.social/tags/archlinuxpackageupdate" + }, + { + "name": "archlinux", + "url": "https://mastodon.social/tags/archlinux" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://www.archlinux.org/packages/community/any/firefox-tridactyl/", + "title": "Arch Linux - firefox-tridactyl 1.20.1-1 (any)", + "description": "", + "type": "link", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 0, + "height": 0, + "image": null, + "embed_url": "", + "blurhash": null + }, + "poll": null + }, + { + "id": "104707095805517168", + "created_at": "2020-08-17T22:31:21.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://pokemon.men/users/archlinux/statuses/104707095335226017", + "url": "https://pokemon.men/@archlinux/104707095335226017", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

#ArchLinux MacbookAri 2008 taking too long to boot https://bbs.archlinux.org/viewtopic.php?id=258046&action=new #Linux

", + "reblog": null, + "account": { + "id": "803769", + "username": "archlinux", + "acct": "archlinux@pokemon.men", + "display_name": "ArchLinux :archlinux:", + "locked": false, + "bot": true, + "discoverable": true, + "group": false, + "created_at": "2019-05-06T09:29:27.890Z", + "note": "

#ArchLinux #Linux

", + "url": "https://pokemon.men/@archlinux", + "avatar": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 551, + "following_count": 3, + "statuses_count": 36276, + "last_status_at": "2020-08-18", + "emojis": [ + { + "shortcode": "archlinux", + "url": "https://files.mastodon.social/custom_emojis/images/000/098/720/original/df864361dfd59662.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/098/720/static/df864361dfd59662.png", + "visible_in_picker": true + } + ], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "archlinux", + "url": "https://mastodon.social/tags/archlinux" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://bbs.archlinux.org/viewtopic.php?id=258046&action=new", + "title": "MacbookAri 2008 taking too long to boot / Newbie Corner / Arch Linux Forums", + "description": "", + "type": "link", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 0, + "height": 0, + "image": null, + "embed_url": "", + "blurhash": null + }, + "poll": null + }, + { + "id": "104707095521038972", + "created_at": "2020-08-17T22:31:20.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://pokemon.men/users/archlinux/statuses/104707095306448087", + "url": "https://pokemon.men/@archlinux/104707095306448087", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

#ArchLinux Microphone not working on Thinkpad T14s https://bbs.archlinux.org/viewtopic.php?id=257433&action=new #Linux

", + "reblog": null, + "account": { + "id": "803769", + "username": "archlinux", + "acct": "archlinux@pokemon.men", + "display_name": "ArchLinux :archlinux:", + "locked": false, + "bot": true, + "discoverable": true, + "group": false, + "created_at": "2019-05-06T09:29:27.890Z", + "note": "

#ArchLinux #Linux

", + "url": "https://pokemon.men/@archlinux", + "avatar": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 551, + "following_count": 3, + "statuses_count": 36276, + "last_status_at": "2020-08-18", + "emojis": [ + { + "shortcode": "archlinux", + "url": "https://files.mastodon.social/custom_emojis/images/000/098/720/original/df864361dfd59662.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/098/720/static/df864361dfd59662.png", + "visible_in_picker": true + } + ], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "archlinux", + "url": "https://mastodon.social/tags/archlinux" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://bbs.archlinux.org/viewtopic.php?id=257433&action=new", + "title": "Microphone not working on Thinkpad T14s / Multimedia and Games / Arch Linux Forums", + "description": "", + "type": "link", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 0, + "height": 0, + "image": null, + "embed_url": "", + "blurhash": null + }, + "poll": null + }, + { + "id": "104706977691450361", + "created_at": "2020-08-17T22:01:17.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://pokemon.men/users/archlinux/statuses/104706977114433691", + "url": "https://pokemon.men/@archlinux/104706977114433691", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

#ArchLinuxPackageUpdate archlinux-keyring 20200817-1 any https://www.archlinux.org/packages/testing/any/archlinux-keyring/ #ArchLinux #Linux

", + "reblog": null, + "account": { + "id": "803769", + "username": "archlinux", + "acct": "archlinux@pokemon.men", + "display_name": "ArchLinux :archlinux:", + "locked": false, + "bot": true, + "discoverable": true, + "group": false, + "created_at": "2019-05-06T09:29:27.890Z", + "note": "

#ArchLinux #Linux

", + "url": "https://pokemon.men/@archlinux", + "avatar": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 551, + "following_count": 3, + "statuses_count": 36276, + "last_status_at": "2020-08-18", + "emojis": [ + { + "shortcode": "archlinux", + "url": "https://files.mastodon.social/custom_emojis/images/000/098/720/original/df864361dfd59662.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/098/720/static/df864361dfd59662.png", + "visible_in_picker": true + } + ], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "archlinuxpackageupdate", + "url": "https://mastodon.social/tags/archlinuxpackageupdate" + }, + { + "name": "archlinux", + "url": "https://mastodon.social/tags/archlinux" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://www.archlinux.org/packages/testing/any/archlinux-keyring/", + "title": "Arch Linux - archlinux-keyring 20200817-1 (any)", + "description": "", + "type": "link", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 0, + "height": 0, + "image": null, + "embed_url": "", + "blurhash": null + }, + "poll": null + }, + { + "id": "104706977273819021", + "created_at": "2020-08-17T22:01:16.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "zhCn", + "uri": "https://pokemon.men/users/archlinux/statuses/104706977076765589", + "url": "https://pokemon.men/@archlinux/104706977076765589", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

#ArchLinuxPackageUpdate libwebrtc 83.git1.18721df-1 x86_64 https://www.archlinux.org/packages/community-testing/x86_64/libwebrtc/ #ArchLinux #Linux

", + "reblog": null, + "account": { + "id": "803769", + "username": "archlinux", + "acct": "archlinux@pokemon.men", + "display_name": "ArchLinux :archlinux:", + "locked": false, + "bot": true, + "discoverable": true, + "group": false, + "created_at": "2019-05-06T09:29:27.890Z", + "note": "

#ArchLinux #Linux

", + "url": "https://pokemon.men/@archlinux", + "avatar": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 551, + "following_count": 3, + "statuses_count": 36276, + "last_status_at": "2020-08-18", + "emojis": [ + { + "shortcode": "archlinux", + "url": "https://files.mastodon.social/custom_emojis/images/000/098/720/original/df864361dfd59662.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/098/720/static/df864361dfd59662.png", + "visible_in_picker": true + } + ], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "archlinuxpackageupdate", + "url": "https://mastodon.social/tags/archlinuxpackageupdate" + }, + { + "name": "archlinux", + "url": "https://mastodon.social/tags/archlinux" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://www.archlinux.org/packages/community-testing/x86_64/libwebrtc/", + "title": "Arch Linux - libwebrtc 83.git1.18721df-1 (x86_64)", + "description": "", + "type": "link", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 0, + "height": 0, + "image": null, + "embed_url": "", + "blurhash": null + }, + "poll": null + }, + { + "id": "104706937982155569", + "created_at": "2020-08-17T21:51:16.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://pokemon.men/users/archlinux/statuses/104706937752475179", + "url": "https://pokemon.men/@archlinux/104706937752475179", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "content": "

#ArchLinuxPackageUpdate containerd 1.4.0-2 x86_64 https://www.archlinux.org/packages/community/x86_64/containerd/ #ArchLinux #Linux

", + "reblog": null, + "account": { + "id": "803769", + "username": "archlinux", + "acct": "archlinux@pokemon.men", + "display_name": "ArchLinux :archlinux:", + "locked": false, + "bot": true, + "discoverable": true, + "group": false, + "created_at": "2019-05-06T09:29:27.890Z", + "note": "

#ArchLinux #Linux

", + "url": "https://pokemon.men/@archlinux", + "avatar": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/803/769/original/63c983e058954ae1.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 551, + "following_count": 3, + "statuses_count": 36276, + "last_status_at": "2020-08-18", + "emojis": [ + { + "shortcode": "archlinux", + "url": "https://files.mastodon.social/custom_emojis/images/000/098/720/original/df864361dfd59662.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/098/720/static/df864361dfd59662.png", + "visible_in_picker": true + } + ], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [ + { + "name": "archlinuxpackageupdate", + "url": "https://mastodon.social/tags/archlinuxpackageupdate" + }, + { + "name": "archlinux", + "url": "https://mastodon.social/tags/archlinux" + }, + { + "name": "linux", + "url": "https://mastodon.social/tags/linux" + } + ], + "emojis": [], + "card": { + "url": "https://www.archlinux.org/packages/community/x86_64/containerd/", + "title": "Arch Linux - containerd 1.4.0-2 (x86_64)", + "description": "", + "type": "link", + "author_name": "", + "author_url": "", + "provider_name": "", + "provider_url": "", + "html": "", + "width": 0, + "height": 0, + "image": null, + "embed_url": "", + "blurhash": null + }, + "poll": null + } +] diff --git a/Development Assets/DevelopmentModels.swift b/Development Assets/DevelopmentModels.swift index 9ef14d6..e578d20 100644 --- a/Development Assets/DevelopmentModels.swift +++ b/Development Assets/DevelopmentModels.swift @@ -47,7 +47,9 @@ extension AppEnvironment { static let development = AppEnvironment( session: Session(configuration: .stubbing), webAuthSessionType: SuccessfulMockWebAuthSession.self, - keychainServiceType: MockKeychainService.self) + keychainServiceType: MockKeychainService.self, + userDefaults: MockUserDefaults(), + inMemoryContent: true) } extension IdentitiesService { @@ -110,4 +112,8 @@ extension NotificationTypesPreferencesViewModel { static let development = NotificationTypesPreferencesViewModel(identityService: .development) } +extension StatusesViewModel { + static let development = StatusesViewModel(statusListService: IdentityService.development.service(timeline: .home)) +} + // swiftlint:enable force_try diff --git a/Development Assets/Mastodon API Stubs/TimelinesEndpoint+Stubbing.swift b/Development Assets/Mastodon API Stubs/TimelinesEndpoint+Stubbing.swift new file mode 100644 index 0000000..d4b35a7 --- /dev/null +++ b/Development Assets/Mastodon API Stubs/TimelinesEndpoint+Stubbing.swift @@ -0,0 +1,14 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif + +extension TimelinesEndpoint: Stubbing { + func data(url: URL) -> Data? { + NSDataAsset(name: "TimelineJSON")!.data + } +} diff --git a/Development Assets/Stubbing.swift b/Development Assets/Stubbing.swift index c0e17e3..c86569e 100644 --- a/Development Assets/Stubbing.swift +++ b/Development Assets/Stubbing.swift @@ -30,5 +30,7 @@ extension Stubbing { dataString(url: url)?.data(using: .utf8) } + func dataString(url: URL) -> String? { nil } + func statusCode(url: URL) -> Int? { 200 } } diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index 589cc00..281dccb 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -61,6 +61,34 @@ D052BBCB24D74C9300A80A7A /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBC824D74B6400A80A7A /* MockUserDefaults.swift */; }; D052BBD124D750CA00A80A7A /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCC24D750A100A80A7A /* AppEnvironment.swift */; }; D052BBD224D750CB00A80A7A /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCC24D750A100A80A7A /* AppEnvironment.swift */; }; + D05494E424EA3EF7008B00A5 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494E324EA3EF7008B00A5 /* Tag.swift */; }; + D05494E524EA3EF7008B00A5 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494E324EA3EF7008B00A5 /* Tag.swift */; }; + D05494E724EA3F1A008B00A5 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494E624EA3F1A008B00A5 /* Mention.swift */; }; + D05494E824EA3F1A008B00A5 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494E624EA3F1A008B00A5 /* Mention.swift */; }; + D05494EA24EA3F54008B00A5 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494E924EA3F54008B00A5 /* Attachment.swift */; }; + D05494EB24EA3F54008B00A5 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494E924EA3F54008B00A5 /* Attachment.swift */; }; + D05494ED24EA3FA9008B00A5 /* Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494EC24EA3FA9008B00A5 /* Poll.swift */; }; + D05494EE24EA3FA9008B00A5 /* Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494EC24EA3FA9008B00A5 /* Poll.swift */; }; + D05494F024EA3FE5008B00A5 /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494EF24EA3FE5008B00A5 /* Card.swift */; }; + D05494F124EA3FE5008B00A5 /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494EF24EA3FE5008B00A5 /* Card.swift */; }; + D05494F724EA49F7008B00A5 /* TimelinesEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494F624EA49F7008B00A5 /* TimelinesEndpoint.swift */; }; + D05494F824EA49F7008B00A5 /* TimelinesEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494F624EA49F7008B00A5 /* TimelinesEndpoint.swift */; }; + D05494FA24EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494F924EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift */; }; + D05494FB24EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494F924EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift */; }; + D054950124EA4FFE008B00A5 /* DevelopmentAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D054950024EA4FFE008B00A5 /* DevelopmentAssets.xcassets */; }; + D054950224EA4FFE008B00A5 /* DevelopmentAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D054950024EA4FFE008B00A5 /* DevelopmentAssets.xcassets */; }; + D054951224EB1041008B00A5 /* StatusListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D054951124EB1041008B00A5 /* StatusListService.swift */; }; + D054951324EB1041008B00A5 /* StatusListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D054951124EB1041008B00A5 /* StatusListService.swift */; }; + D054951524EB1053008B00A5 /* TimelineService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D054951424EB1053008B00A5 /* TimelineService.swift */; }; + D054951624EB1053008B00A5 /* TimelineService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D054951424EB1053008B00A5 /* TimelineService.swift */; }; + D054951B24EB2825008B00A5 /* TransientStatusCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D054951A24EB2825008B00A5 /* TransientStatusCollection.swift */; }; + D054951C24EB2825008B00A5 /* TransientStatusCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D054951A24EB2825008B00A5 /* TransientStatusCollection.swift */; }; + D057426724E9FE1D00839EBA /* ContentDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D057426624E9FE1D00839EBA /* ContentDatabase.swift */; }; + D057426824E9FE1D00839EBA /* ContentDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D057426624E9FE1D00839EBA /* ContentDatabase.swift */; }; + D057426A24EA32AC00839EBA /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D057426924EA32AC00839EBA /* Timeline.swift */; }; + D057426B24EA32AC00839EBA /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D057426924EA32AC00839EBA /* Timeline.swift */; }; + D057426D24EA339300839EBA /* ListTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D057426C24EA339300839EBA /* ListTimeline.swift */; }; + D057426E24EA339300839EBA /* ListTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D057426C24EA339300839EBA /* ListTimeline.swift */; }; D065F53924D37E5100741304 /* CombineExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = D065F53824D37E5100741304 /* CombineExpectations */; }; D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53A24D3B33A00741304 /* View+Extensions.swift */; }; D065F53C24D3B33A00741304 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53A24D3B33A00741304 /* View+Extensions.swift */; }; @@ -102,10 +130,10 @@ D0BEC93924C9632800E864C4 /* RootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93724C9632800E864C4 /* RootViewModel.swift */; }; D0BEC93B24C96FD500E864C4 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93A24C96FD500E864C4 /* RootView.swift */; }; D0BEC93C24C96FD500E864C4 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93A24C96FD500E864C4 /* RootView.swift */; }; - D0BEC94724CA22C400E864C4 /* TimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */; }; - D0BEC94824CA22C400E864C4 /* TimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */; }; - D0BEC94A24CA231200E864C4 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94924CA231200E864C4 /* TimelineView.swift */; }; - D0BEC94B24CA231200E864C4 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94924CA231200E864C4 /* TimelineView.swift */; }; + D0BEC94724CA22C400E864C4 /* StatusesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94624CA22C400E864C4 /* StatusesViewModel.swift */; }; + D0BEC94824CA22C400E864C4 /* StatusesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94624CA22C400E864C4 /* StatusesViewModel.swift */; }; + D0BEC94A24CA231200E864C4 /* StatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94924CA231200E864C4 /* StatusesView.swift */; }; + D0BEC94B24CA231200E864C4 /* StatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94924CA231200E864C4 /* StatusesView.swift */; }; D0C963FB24CC359D003BD330 /* AlertItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C963FA24CC359D003BD330 /* AlertItem.swift */; }; D0C963FC24CC359D003BD330 /* AlertItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C963FA24CC359D003BD330 /* AlertItem.swift */; }; D0C963FE24CC3812003BD330 /* Publisher+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */; }; @@ -258,6 +286,20 @@ D052BBC624D749C800A80A7A /* RootViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModelTests.swift; sourceTree = ""; }; D052BBC824D74B6400A80A7A /* MockUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserDefaults.swift; sourceTree = ""; }; D052BBCC24D750A100A80A7A /* AppEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEnvironment.swift; sourceTree = ""; }; + D05494E324EA3EF7008B00A5 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; + D05494E624EA3F1A008B00A5 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = ""; }; + D05494E924EA3F54008B00A5 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; + D05494EC24EA3FA9008B00A5 /* Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poll.swift; sourceTree = ""; }; + D05494EF24EA3FE5008B00A5 /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; + D05494F624EA49F7008B00A5 /* TimelinesEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesEndpoint.swift; sourceTree = ""; }; + D05494F924EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimelinesEndpoint+Stubbing.swift"; sourceTree = ""; }; + D054950024EA4FFE008B00A5 /* DevelopmentAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DevelopmentAssets.xcassets; sourceTree = ""; }; + D054951124EB1041008B00A5 /* StatusListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListService.swift; sourceTree = ""; }; + D054951424EB1053008B00A5 /* TimelineService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineService.swift; sourceTree = ""; }; + D054951A24EB2825008B00A5 /* TransientStatusCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransientStatusCollection.swift; sourceTree = ""; }; + D057426624E9FE1D00839EBA /* ContentDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentDatabase.swift; sourceTree = ""; }; + D057426924EA32AC00839EBA /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = ""; }; + D057426C24EA339300839EBA /* ListTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTimeline.swift; sourceTree = ""; }; D065F53A24D3B33A00741304 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D0666A2524C677B400F3F04B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -278,8 +320,8 @@ D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+Extensions.swift"; sourceTree = ""; }; D0BEC93724C9632800E864C4 /* RootViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModel.swift; sourceTree = ""; }; D0BEC93A24C96FD500E864C4 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; - D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModel.swift; sourceTree = ""; }; - D0BEC94924CA231200E864C4 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = ""; }; + D0BEC94624CA22C400E864C4 /* StatusesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusesViewModel.swift; sourceTree = ""; }; + D0BEC94924CA231200E864C4 /* StatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusesView.swift; sourceTree = ""; }; D0C963FA24CC359D003BD330 /* AlertItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertItem.swift; sourceTree = ""; }; D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Extensions.swift"; sourceTree = ""; }; D0CD847224DBDEC700CF380C /* MastodonPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPreferences.swift; sourceTree = ""; }; @@ -409,6 +451,7 @@ D019E6E024DF72E700697C7D /* InstanceEndpoint.swift */, D019E6DD24DF72E700697C7D /* PreferencesEndpoint.swift */, D0EC8DED24E2704D00A08489 /* PushSubscriptionEndpoint.swift */, + D05494F624EA49F7008B00A5 /* TimelinesEndpoint.swift */, ); path = Endpoints; sourceTree = ""; @@ -418,6 +461,7 @@ children = ( D019E6EF24DF7C2F00697C7D /* DatabaseError.swift */, D019E6EC24DF7BF300697C7D /* IdentityDatabase.swift */, + D057426624E9FE1D00839EBA /* ContentDatabase.swift */, ); path = Databases; sourceTree = ""; @@ -425,12 +469,13 @@ D019E6F224DF7C9E00697C7D /* Services */ = { isa = PBXGroup; children = ( + D054951024EB101F008B00A5 /* Status List Services */, D0EC8DCD24DFB64200A08489 /* AuthenticationService.swift */, D0EC8DCA24DFA06700A08489 /* IdentitiesService.swift */, D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */, D0EC8DC424DF842700A08489 /* KeychainService.swift */, - D0EC8DD724E096C900A08489 /* UserNotificationService.swift */, D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */, + D0EC8DD724E096C900A08489 /* UserNotificationService.swift */, ); path = Services; sourceTree = ""; @@ -500,6 +545,15 @@ path = macOS; sourceTree = ""; }; + D054951024EB101F008B00A5 /* Status List Services */ = { + isa = PBXGroup; + children = ( + D054951124EB1041008B00A5 /* StatusListService.swift */, + D054951424EB1053008B00A5 /* TimelineService.swift */, + ); + path = "Status List Services"; + sourceTree = ""; + }; D0666A2224C677B400F3F04B /* Tests */ = { isa = PBXGroup; children = ( @@ -519,14 +573,22 @@ D0666A6224C6DC6C00F3F04B /* AppAuthorization.swift */, D052BBCC24D750A100A80A7A /* AppEnvironment.swift */, D0ED1BD624CF94B200B4899C /* Application.swift */, + D05494E924EA3F54008B00A5 /* Attachment.swift */, + D05494EF24EA3FE5008B00A5 /* Card.swift */, D0666A5324C6C3E500F3F04B /* Emoji.swift */, D0666A4A24C6C37700F3F04B /* Identity.swift */, D0666A4D24C6C39600F3F04B /* Instance.swift */, + D057426C24EA339300839EBA /* ListTimeline.swift */, D0ED1BE224CFA84400B4899C /* MastodonError.swift */, D0CD847224DBDEC700CF380C /* MastodonPreferences.swift */, + D05494E624EA3F1A008B00A5 /* Mention.swift */, + D05494EC24EA3FA9008B00A5 /* Poll.swift */, D0E5362F24E5436C00FB1CE1 /* PushNotification.swift */, D0EC8DEA24E26F1100A08489 /* PushSubscription.swift */, D0CD847524DBDF3C00CF380C /* Status.swift */, + D05494E324EA3EF7008B00A5 /* Tag.swift */, + D057426924EA32AC00839EBA /* Timeline.swift */, + D054951A24EB2825008B00A5 /* TransientStatusCollection.swift */, D0CD847B24DBEA9F00CF380C /* Unknowable.swift */, ); path = Model; @@ -555,7 +617,7 @@ D0091B6724DC10B30040E8D2 /* PostingReadingPreferencesView.swift */, D0091B6D24DD68090040E8D2 /* PreferencesView.swift */, D0BEC93A24C96FD500E864C4 /* RootView.swift */, - D0BEC94924CA231200E864C4 /* TimelineView.swift */, + D0BEC94924CA231200E864C4 /* StatusesView.swift */, ); path = Views; sourceTree = ""; @@ -579,7 +641,7 @@ D0091B6A24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift */, D0091B7024DD68220040E8D2 /* PreferencesViewModel.swift */, D0BEC93724C9632800E864C4 /* RootViewModel.swift */, - D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */, + D0BEC94624CA22C400E864C4 /* StatusesViewModel.swift */, ); path = "View Models"; sourceTree = ""; @@ -607,6 +669,7 @@ D04FD73B24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift */, D0DC175124D008E300A75C65 /* MastodonTarget+Stubbing.swift */, D0A652AC24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift */, + D05494F924EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift */, ); path = "Mastodon API Stubs"; sourceTree = ""; @@ -641,6 +704,7 @@ D0ED1BB224CE3A1600B4899C /* Development Assets */ = { isa = PBXGroup; children = ( + D054950024EA4FFE008B00A5 /* DevelopmentAssets.xcassets */, D04FD74124D4AA34007D572D /* DevelopmentModels.swift */, D0DC175724D0130800A75C65 /* HTTPStubs.swift */, D0DC174824CFF13700A75C65 /* Mastodon API Stubs */, @@ -817,6 +881,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D054950124EA4FFE008B00A5 /* DevelopmentAssets.xcassets in Resources */, D047FAB224C3E21200AF17C5 /* Assets.xcassets in Resources */, D06B491F24D3F7FE00642749 /* Localizable.strings in Resources */, ); @@ -826,6 +891,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D054950224EA4FFE008B00A5 /* DevelopmentAssets.xcassets in Resources */, D047FAB324C3E21200AF17C5 /* Assets.xcassets in Resources */, D06B492024D3FB8000642749 /* Localizable.strings in Resources */, ); @@ -890,21 +956,27 @@ buildActionMask = 2147483647; files = ( D04FD73924D4A7B4007D572D /* AccountEndpoint+Stubbing.swift in Sources */, + D05494F724EA49F7008B00A5 /* TimelinesEndpoint.swift in Sources */, + D054951B24EB2825008B00A5 /* TransientStatusCollection.swift in Sources */, D0DB6F0924C65AC000D965FE /* AddIdentityViewModel.swift in Sources */, D0CD847324DBDEC700CF380C /* MastodonPreferences.swift in Sources */, D075817924E6657B0081F6A3 /* NotificationTypesPreferencesViewModel.swift in Sources */, D0ED1BD724CF94B200B4899C /* Application.swift in Sources */, D047FAAE24C3E21200AF17C5 /* MetatextApp.swift in Sources */, - D0BEC94724CA22C400E864C4 /* TimelineViewModel.swift in Sources */, + D0BEC94724CA22C400E864C4 /* StatusesViewModel.swift in Sources */, D0666A4E24C6C39600F3F04B /* Instance.swift in Sources */, + D057426724E9FE1D00839EBA /* ContentDatabase.swift in Sources */, + D054951524EB1053008B00A5 /* TimelineService.swift in Sources */, D019E6E924DF72E700697C7D /* InstanceEndpoint.swift in Sources */, D0ED1BE324CFA84400B4899C /* MastodonError.swift in Sources */, D0EC8DE824E21FEC00A08489 /* Data+Extensions.swift in Sources */, D0666A6324C6DC6C00F3F04B /* AppAuthorization.swift in Sources */, + D05494F024EA3FE5008B00A5 /* Card.swift in Sources */, D019E6E524DF72E700697C7D /* AccountEndpoint.swift in Sources */, D075817C24E6659A0081F6A3 /* NotificationTypesPreferencesView.swift in Sources */, D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */, D0DC174A24CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift in Sources */, + D05494E424EA3EF7008B00A5 /* Tag.swift in Sources */, D0159F8A24DE742F00E78478 /* IdentitiesViewModel.swift in Sources */, D0666A5124C6C3BC00F3F04B /* Account.swift in Sources */, D019E6E124DF72E700697C7D /* AppAuthorizationEndpoint.swift in Sources */, @@ -920,13 +992,15 @@ D019E6E324DF72E700697C7D /* PreferencesEndpoint.swift in Sources */, D0666A4B24C6C37700F3F04B /* Identity.swift in Sources */, D0666A5424C6C3E500F3F04B /* Emoji.swift in Sources */, + D05494FA24EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift in Sources */, D0A652AD24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift in Sources */, D0DC175524D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */, D0B23F0D24D210E90066F411 /* NSError+Extensions.swift in Sources */, + D05494ED24EA3FA9008B00A5 /* Poll.swift in Sources */, D019E6D924DF728400697C7D /* MastodonDecoder.swift in Sources */, D052BBCA24D74C9200A80A7A /* MockUserDefaults.swift in Sources */, D0DC175224D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */, - D0BEC94A24CA231200E864C4 /* TimelineView.swift in Sources */, + D0BEC94A24CA231200E864C4 /* StatusesView.swift in Sources */, D0BEC93B24C96FD500E864C4 /* RootView.swift in Sources */, D04FD74224D4AA34007D572D /* DevelopmentModels.swift in Sources */, D0EC8DC524DF842700A08489 /* KeychainService.swift in Sources */, @@ -941,6 +1015,7 @@ D0EC8DE524E0B44500A08489 /* UserNotificationService.swift in Sources */, D0EC8DCB24DFA06700A08489 /* IdentitiesService.swift in Sources */, D0091B7124DD68220040E8D2 /* PreferencesViewModel.swift in Sources */, + D057426A24EA32AC00839EBA /* Timeline.swift in Sources */, D0DC174D24CFF1F100A75C65 /* Stubbing.swift in Sources */, D0091B6B24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift in Sources */, D0159F8624DE742F00E78478 /* TabNavigationViewModel.swift in Sources */, @@ -952,13 +1027,16 @@ D0DB6EF424C5228A00D965FE /* AddIdentityView.swift in Sources */, D074577724D29006004758DB /* MockWebAuthSession.swift in Sources */, D0159FA524DE989700E78478 /* NSMutableAttributedString+Extensions.swift in Sources */, + D057426D24EA339300839EBA /* ListTimeline.swift in Sources */, D0ED1BCE24CF768200B4899C /* MastodonEndpoint.swift in Sources */, D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */, D0ED1BB724CE47F400B4899C /* WebAuthSession.swift in Sources */, D0A1CA7424DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */, + D054951224EB1041008B00A5 /* StatusListService.swift in Sources */, D0159F9124DE743700E78478 /* TabNavigationView.swift in Sources */, D0ED1BC424CED54D00B4899C /* HTTPTarget.swift in Sources */, D0EC8DC824DF8B3C00A08489 /* SecretsService.swift in Sources */, + D05494E724EA3F1A008B00A5 /* Mention.swift in Sources */, D0159FA324DE955900E78478 /* CustomEmojiText.swift in Sources */, D0C963FE24CC3812003BD330 /* Publisher+Extensions.swift in Sources */, D0EC8DCE24DFB64200A08489 /* AuthenticationService.swift in Sources */, @@ -969,6 +1047,7 @@ D0666A6F24C6DFB300F3F04B /* AccessToken.swift in Sources */, D0ED1BCB24CF744200B4899C /* MastodonClient.swift in Sources */, D0091B6824DC10B30040E8D2 /* PostingReadingPreferencesView.swift in Sources */, + D05494EA24EA3F54008B00A5 /* Attachment.swift in Sources */, D0CD847624DBDF3C00CF380C /* Status.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -983,7 +1062,7 @@ D0CD847424DBDEC700CF380C /* MastodonPreferences.swift in Sources */, D0ED1BD824CF94B200B4899C /* Application.swift in Sources */, D047FAAF24C3E21200AF17C5 /* MetatextApp.swift in Sources */, - D0BEC94824CA22C400E864C4 /* TimelineViewModel.swift in Sources */, + D0BEC94824CA22C400E864C4 /* StatusesViewModel.swift in Sources */, D0159FA624DE98F600E78478 /* NSMutableAttributedString+Extensions.swift in Sources */, D0EC8DC324DF7D9C00A08489 /* IdentityService.swift in Sources */, D0666A4F24C6C39600F3F04B /* Instance.swift in Sources */, @@ -992,6 +1071,8 @@ D065F53C24D3B33A00741304 /* View+Extensions.swift in Sources */, D0DC174B24CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift in Sources */, D0666A5224C6C3BC00F3F04B /* Account.swift in Sources */, + D05494EE24EA3FA9008B00A5 /* Poll.swift in Sources */, + D05494FB24EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift in Sources */, D052BBD124D750CA00A80A7A /* AppEnvironment.swift in Sources */, D081A40624D0F1A8001B016E /* String+Extensions.swift in Sources */, D0BEC93924C9632800E864C4 /* RootViewModel.swift in Sources */, @@ -1008,18 +1089,25 @@ D052BBCB24D74C9300A80A7A /* MockUserDefaults.swift in Sources */, D0DC175324D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */, D0EC8DE924E21FEC00A08489 /* Data+Extensions.swift in Sources */, - D0BEC94B24CA231200E864C4 /* TimelineView.swift in Sources */, + D0BEC94B24CA231200E864C4 /* StatusesView.swift in Sources */, + D05494E524EA3EF7008B00A5 /* Tag.swift in Sources */, D0BEC93C24C96FD500E864C4 /* RootView.swift in Sources */, D0159F9B24DE748900E78478 /* SidebarNavigationViewModel.swift in Sources */, D04FD74324D4AA34007D572D /* DevelopmentModels.swift in Sources */, D0DC175924D0130800A75C65 /* HTTPStubs.swift in Sources */, + D05494E824EA3F1A008B00A5 /* Mention.swift in Sources */, + D057426B24EA32AC00839EBA /* Timeline.swift in Sources */, D019E6DA24DF728400697C7D /* MastodonDecoder.swift in Sources */, + D05494EB24EA3F54008B00A5 /* Attachment.swift in Sources */, D0DC177824D0CF2600A75C65 /* MockKeychainService.swift in Sources */, D0C963FC24CC359D003BD330 /* AlertItem.swift in Sources */, D019E6E224DF72E700697C7D /* AppAuthorizationEndpoint.swift in Sources */, D0DC174724CFEC2000A75C65 /* StubbingURLProtocol.swift in Sources */, + D05494F824EA49F7008B00A5 /* TimelinesEndpoint.swift in Sources */, D0091B7224DD68220040E8D2 /* PreferencesViewModel.swift in Sources */, + D054951624EB1053008B00A5 /* TimelineService.swift in Sources */, D019E6D824DF728400697C7D /* MastodonEncoder.swift in Sources */, + D054951C24EB2825008B00A5 /* TransientStatusCollection.swift in Sources */, D0DC174E24CFF1F100A75C65 /* Stubbing.swift in Sources */, D0091B6C24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift in Sources */, D0091B6F24DD68090040E8D2 /* PreferencesView.swift in Sources */, @@ -1028,9 +1116,11 @@ D0DB6EF524C5233E00D965FE /* AddIdentityView.swift in Sources */, D019E6EA24DF72E700697C7D /* InstanceEndpoint.swift in Sources */, D0EC8DCF24DFB64200A08489 /* AuthenticationService.swift in Sources */, + D054951324EB1041008B00A5 /* StatusListService.swift in Sources */, D0159F9C24DE748C00E78478 /* SidebarNavigationView.swift in Sources */, D019E6F124DF7C2F00697C7D /* DatabaseError.swift in Sources */, D074577824D29006004758DB /* MockWebAuthSession.swift in Sources */, + D057426E24EA339300839EBA /* ListTimeline.swift in Sources */, D0ED1BCF24CF768200B4899C /* MastodonEndpoint.swift in Sources */, D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */, D0ED1BB824CE47F400B4899C /* WebAuthSession.swift in Sources */, @@ -1050,6 +1140,8 @@ D0ED1BCC24CF744200B4899C /* MastodonClient.swift in Sources */, D0091B6924DC10B30040E8D2 /* PostingReadingPreferencesView.swift in Sources */, D075817A24E6657B0081F6A3 /* NotificationTypesPreferencesViewModel.swift in Sources */, + D05494F124EA3FE5008B00A5 /* Card.swift in Sources */, + D057426824E9FE1D00839EBA /* ContentDatabase.swift in Sources */, D019E6E624DF72E700697C7D /* AccountEndpoint.swift in Sources */, D0CD847724DBDF3C00CF380C /* Status.swift in Sources */, D0EC8DEF24E2704D00A08489 /* PushSubscriptionEndpoint.swift in Sources */, diff --git a/Shared/Databases/ContentDatabase.swift b/Shared/Databases/ContentDatabase.swift new file mode 100644 index 0000000..209d872 --- /dev/null +++ b/Shared/Databases/ContentDatabase.swift @@ -0,0 +1,410 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import Combine +import GRDB + +// swiftlint:disable file_length +struct ContentDatabase { + private let databaseQueue: DatabaseQueue + + init(identityID: UUID, inMemory: Bool = false) throws { + guard + let documentsDirectory = NSSearchPathForDirectoriesInDomains( + .documentDirectory, + .userDomainMask, true) + .first + else { throw DatabaseError.documentsDirectoryNotFound } + + if inMemory { + databaseQueue = DatabaseQueue() + } else { + databaseQueue = try DatabaseQueue(path: "\(documentsDirectory)/\(identityID.uuidString).sqlite3") + } + + try Self.migrate(databaseQueue) + try Self.createTemporaryTables(databaseQueue) + } +} + +extension ContentDatabase { + func insert(statuses: [Status], collection: StatusCollection? = nil) -> AnyPublisher { + databaseQueue.writePublisher { + try collection?.save($0) + + for status in statuses { + for component in status.storedComponents() { + try component.save($0) + } + + try collection?.joinRecord(status: status).save($0) + } + } + .eraseToAnyPublisher() + } + + func statusesObservation(timeline: Timeline) -> AnyPublisher<[Status], Error> { + ValueObservation + .tracking(timeline.statuses + .including(required: StoredStatus.account) + .including(optional: StoredStatus.reblogAccount) + .including(optional: StoredStatus.reblog) + .asRequest(of: StatusResult.self) + .fetchAll) + .removeDuplicates() + .publisher(in: databaseQueue) + .map { $0.map(Status.init(statusResult:)) } + .eraseToAnyPublisher() + } +} + +private extension ContentDatabase { + // swiftlint:disable function_body_length + static func migrate(_ writer: DatabaseWriter) throws { + var migrator = DatabaseMigrator() + + migrator.registerMigration("createStatuses") { db in + try db.create(table: "account", ifNotExists: true) { t in + t.column("id", .text).notNull().primaryKey(onConflict: .replace) + t.column("username", .text).notNull() + t.column("acct", .text).notNull() + t.column("displayName", .text).notNull() + t.column("locked", .boolean).notNull() + t.column("createdAt", .date).notNull() + t.column("followersCount", .integer).notNull() + t.column("followingCount", .integer).notNull() + t.column("statusesCount", .integer).notNull() + t.column("note", .text).notNull() + t.column("url", .text).notNull() + t.column("avatar", .text).notNull() + t.column("avatarStatic", .text).notNull() + t.column("header", .text).notNull() + t.column("headerStatic", .text).notNull() + t.column("fields", .blob).notNull() + t.column("emojis", .blob).notNull() + t.column("bot", .boolean).notNull() + t.column("moved", .boolean) + t.column("discoverable", .boolean) + } + + try db.create(table: "storedStatus", ifNotExists: true) { t in + t.column("id", .text).notNull().primaryKey(onConflict: .replace) + t.column("uri", .text).notNull() + t.column("createdAt", .datetime).notNull() + t.column("accountId", .text).indexed().notNull().references("account", column: "id") + t.column("content", .text).notNull() + t.column("visibility", .text).notNull() + t.column("sensitive", .boolean).notNull() + t.column("spoilerText", .text).notNull() + t.column("mediaAttachments", .blob).notNull() + t.column("mentions", .blob).notNull() + t.column("tags", .blob).notNull() + t.column("emojis", .blob).notNull() + t.column("reblogsCount", .integer).notNull() + t.column("favouritesCount", .integer).notNull() + t.column("repliesCount", .integer).notNull() + t.column("application", .blob) + t.column("url", .text) + t.column("inReplyToId", .text) + t.column("inReplyToAccountId", .text) + t.column("reblogId", .text).indexed().references("storedStatus", column: "id") + t.column("poll", .blob) + t.column("card", .blob) + t.column("language", .text) + t.column("text", .text) + t.column("favourited", .boolean) + t.column("reblogged", .boolean) + t.column("muted", .boolean) + t.column("bookmarked", .boolean) + t.column("pinned", .boolean) + } + + try db.create(table: "timeline", ifNotExists: true) { t in + t.column("id", .text).notNull().primaryKey(onConflict: .replace) + t.column("listTitle", .text) + } + + try db.create(table: "timelineStatusJoin", ifNotExists: true) { t in + t.column("timelineId", .text) + .indexed() + .notNull() + .references("timeline", column: "id", onDelete: .cascade, onUpdate: .cascade) + t.column("statusId", .text) + .indexed() + .notNull() + .references("storedStatus", column: "id", onDelete: .cascade, onUpdate: .cascade) + + t.primaryKey(["timelineId", "statusId"], onConflict: .replace) + } + } + + try migrator.migrate(writer) + } + // swiftlint:enable function_body_length + + static func createTemporaryTables(_ writer: DatabaseWriter) throws { + try writer.write { database in + try database.create(table: "transientStatusCollection", temporary: true, ifNotExists: true) { t in + t.column("id", .text).notNull().primaryKey(onConflict: .replace) + } + + try database.create(table: "transientStatusCollectionElement", temporary: true, ifNotExists: true) { t in + t.column("transientStatusCollectionId", .text) + .notNull() + .references("transientStatusCollection", column: "id", onDelete: .cascade, onUpdate: .cascade) + t.column("statusId", .text).notNull() + + t.primaryKey(["transientStatusCollectionId", "statusId"], onConflict: .replace) + } + } + } +} + +extension Account: TableRecord, FetchableRecord, PersistableRecord { + static func databaseJSONDecoder(for column: String) -> JSONDecoder { + MastodonDecoder() + } + + static func databaseJSONEncoder(for column: String) -> JSONEncoder { + MastodonEncoder() + } +} + +protocol StatusCollection: FetchableRecord, PersistableRecord { + var id: String { get } + + func joinRecord(status: Status) -> PersistableRecord +} + +private struct TimelineStatusJoin: Codable, TableRecord, FetchableRecord, PersistableRecord { + let timelineId: String + let statusId: String + + static let status = belongsTo(StoredStatus.self) +} + +extension Timeline: StatusCollection { + enum Columns: String, ColumnExpression { + case id, listTitle + } + + init(row: Row) { + switch row[Columns.id] as String { + case Timeline.home.id: + self = .home + case Timeline.local.id: + self = .local + case Timeline.federated.id: + self = .federated + default: + self = .list(MastodonList(id: row[Columns.id], title: row[Columns.listTitle])) + } + } + + func encode(to container: inout PersistenceContainer) { + container[Columns.id] = id + + if case let .list(list) = self { + container[Columns.listTitle] = list.title + } + } + + func joinRecord(status: Status) -> PersistableRecord { + TimelineStatusJoin(timelineId: id, statusId: status.id) + } +} + +private extension Timeline { + static let statusJoins = hasMany(TimelineStatusJoin.self) + + static let statuses = hasMany(StoredStatus.self, + through: statusJoins, + using: TimelineStatusJoin.status).order(Column("createdAt").desc) + + var statusJoins: QueryInterfaceRequest { + request(for: Self.statusJoins) + } + + var statuses: QueryInterfaceRequest { + request(for: Self.statuses) + } +} + +private struct TransientStatusCollectionElement: Codable, TableRecord, FetchableRecord, PersistableRecord { + let transientStatusCollectionId: String + let statusId: String + + static let status = belongsTo(StoredStatus.self, key: "statusId") +} + +extension TransientStatusCollection: StatusCollection { + func joinRecord(status: Status) -> PersistableRecord { + TransientStatusCollectionElement(transientStatusCollectionId: id, statusId: status.id) + } +} + +private extension TransientStatusCollection { + static let elements = hasMany(TransientStatusCollectionElement.self) + + var elements: QueryInterfaceRequest { + request(for: Self.elements) + } +} + +private struct StoredStatus: Codable, Hashable { + let id: String + let uri: String + let createdAt: Date + let accountId: String + let content: String + let visibility: Status.Visibility + let sensitive: Bool + let spoilerText: String + let mediaAttachments: [Attachment] + let mentions: [Mention] + let tags: [Tag] + let emojis: [Emoji] + let reblogsCount: Int + let favouritesCount: Int + let repliesCount: Int + let application: Application? + let url: URL? + let inReplyToId: String? + let inReplyToAccountId: String? + let reblogId: String? + let poll: Poll? + let card: Card? + let language: String? + let text: String? + let favourited: Bool? + let reblogged: Bool? + let muted: Bool? + let bookmarked: Bool? + let pinned: Bool? +} + +private extension StoredStatus { + static let account = belongsTo(Account.self, key: "account") + static let reblogAccount = hasOne(Account.self, through: Self.reblog, using: Self.account, key: "reblogAccount") + static let reblog = belongsTo(StoredStatus.self, key: "reblog") + + var account: QueryInterfaceRequest { + request(for: Self.account) + } + + var reblogAccount: QueryInterfaceRequest { + request(for: Self.reblogAccount) + } + + var reblog: QueryInterfaceRequest { + request(for: Self.reblog) + } + + init(status: Status) { + id = status.id + uri = status.uri + createdAt = status.createdAt + accountId = status.account.id + content = status.content + visibility = status.visibility + sensitive = status.sensitive + spoilerText = status.spoilerText + mediaAttachments = status.mediaAttachments + mentions = status.mentions + tags = status.tags + emojis = status.emojis + reblogsCount = status.reblogsCount + favouritesCount = status.favouritesCount + repliesCount = status.repliesCount + application = status.application + url = status.url + inReplyToId = status.inReplyToId + inReplyToAccountId = status.inReplyToAccountId + reblogId = status.reblog?.id + poll = status.poll + card = status.card + language = status.language + text = status.text + favourited = status.favourited + reblogged = status.reblogged + muted = status.muted + bookmarked = status.bookmarked + pinned = status.pinned + } +} + +extension StoredStatus: TableRecord, FetchableRecord, PersistableRecord { + static func databaseJSONDecoder(for column: String) -> JSONDecoder { + MastodonDecoder() + } + + static func databaseJSONEncoder(for column: String) -> JSONEncoder { + MastodonEncoder() + } +} + +private struct StatusResult: Codable, Hashable, FetchableRecord { + let account: Account + let status: StoredStatus + let reblogAccount: Account? + let reblog: StoredStatus? +} + +private extension Status { + func storedComponents() -> [PersistableRecord] { + var components: [PersistableRecord] = [account] + + if let reblog = reblog { + components.append(reblog.account) + components.append(StoredStatus(status: reblog)) + } + + components.append(StoredStatus(status: self)) + + return components + } + + convenience init(statusResult: StatusResult) { + var reblog: Status? + + if let reblogResult = statusResult.reblog, let reblogAccount = statusResult.reblogAccount { + reblog = Status(storedStatus: reblogResult, account: reblogAccount, reblog: nil) + } + + self.init(storedStatus: statusResult.status, account: statusResult.account, reblog: reblog) + } + + convenience init(storedStatus: StoredStatus, account: Account, reblog: Status?) { + self.init( + id: storedStatus.id, + uri: storedStatus.uri, + createdAt: storedStatus.createdAt, + account: account, + content: storedStatus.content, + visibility: storedStatus.visibility, + sensitive: storedStatus.sensitive, + spoilerText: storedStatus.spoilerText, + mediaAttachments: storedStatus.mediaAttachments, + mentions: storedStatus.mentions, + tags: storedStatus.tags, + emojis: storedStatus.emojis, + reblogsCount: storedStatus.reblogsCount, + favouritesCount: storedStatus.favouritesCount, + repliesCount: storedStatus.repliesCount, + application: storedStatus.application, + url: storedStatus.url, + inReplyToId: storedStatus.inReplyToId, + inReplyToAccountId: storedStatus.inReplyToAccountId, + reblog: reblog, + poll: storedStatus.poll, + card: storedStatus.card, + language: storedStatus.language, + text: storedStatus.text, + favourited: storedStatus.favourited, + reblogged: storedStatus.reblogged, + muted: storedStatus.muted, + bookmarked: storedStatus.bookmarked, + pinned: storedStatus.pinned) + } +} +// swiftlint:enable file_length diff --git a/Shared/Model/AppEnvironment.swift b/Shared/Model/AppEnvironment.swift index be09c6b..07f88eb 100644 --- a/Shared/Model/AppEnvironment.swift +++ b/Shared/Model/AppEnvironment.swift @@ -6,12 +6,15 @@ struct AppEnvironment { let session: Session let webAuthSessionType: WebAuthSession.Type let keychainServiceType: KeychainService.Type - let userDefaults: UserDefaults = .standard + let userDefaults: UserDefaults + let inMemoryContent: Bool } extension AppEnvironment { static let live: Self = Self( session: Session(configuration: .default), webAuthSessionType: LiveWebAuthSession.self, - keychainServiceType: LiveKeychainService.self) + keychainServiceType: LiveKeychainService.self, + userDefaults: .standard, + inMemoryContent: false) } diff --git a/Shared/Model/Application.swift b/Shared/Model/Application.swift index d42d9dc..4416c8a 100644 --- a/Shared/Model/Application.swift +++ b/Shared/Model/Application.swift @@ -2,7 +2,7 @@ import Foundation -struct Application: Codable { +struct Application: Codable, Hashable { let name: String - let website: String + let website: String? } diff --git a/Shared/Model/Attachment.swift b/Shared/Model/Attachment.swift new file mode 100644 index 0000000..30bf0b2 --- /dev/null +++ b/Shared/Model/Attachment.swift @@ -0,0 +1,43 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +struct Attachment: Codable, Hashable { + enum AttachmentType: String, Codable, Hashable, Unknowable { + case image, video, gifv, audio, unknown + + static var unknownCase: Self { .unknown } + } + + // swiftlint:disable nesting + struct Meta: Codable, Hashable { + struct Info: Codable, Hashable { + let width: Int? + let height: Int? + let size: String? + let aspect: Double? + let frameRate: String? + let duration: Double? + let bitrate: Int? + } + + struct Focus: Codable, Hashable { + let x: Double + let y: Double + } + + let original: Info? + let small: Info? + let focus: Focus? + } + // swiftlint:enable nesting + + let id: String + let type: AttachmentType + let url: URL + let remoteUrl: URL? + let previewUrl: URL + let textUrl: URL? + let meta: Meta? + let description: String? +} diff --git a/Shared/Model/Card.swift b/Shared/Model/Card.swift new file mode 100644 index 0000000..b2539e8 --- /dev/null +++ b/Shared/Model/Card.swift @@ -0,0 +1,25 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +struct Card: Codable, Hashable { + enum CardType: String, Codable, Hashable, Unknowable { + case link, photo, video, rich, unknown + + static var unknownCase: Self { .unknown } + } + + let url: URL + let title: String + let description: String + let type: CardType + let authorName: String? + let authorUrl: String? + let providerName: String? + let providerUrl: String? + let html: String? + let width: Int? + let height: Int? + let image: URL? + let embedUrl: String? +} diff --git a/Shared/Model/ListTimeline.swift b/Shared/Model/ListTimeline.swift new file mode 100644 index 0000000..99c4e2f --- /dev/null +++ b/Shared/Model/ListTimeline.swift @@ -0,0 +1,8 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +struct MastodonList: Codable, Hashable, Identifiable { + let id: String + let title: String +} diff --git a/Shared/Model/Mention.swift b/Shared/Model/Mention.swift new file mode 100644 index 0000000..52720ea --- /dev/null +++ b/Shared/Model/Mention.swift @@ -0,0 +1,10 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +struct Mention: Codable, Hashable { + let url: URL + let username: String + let acct: String + let id: String +} diff --git a/Shared/Model/Poll.swift b/Shared/Model/Poll.swift new file mode 100644 index 0000000..66b938f --- /dev/null +++ b/Shared/Model/Poll.swift @@ -0,0 +1,21 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +struct Poll: Codable, Hashable { + struct Option: Codable, Hashable { + var title: String + var votesCount: Int + } + + let id: String + let expiresAt: Date + let expired: Bool + let multiple: Bool + let votesCount: Int + let votersCount: Int? + let voted: Bool? + let ownVotes: [Int]? + let options: [Option] + let emojis: [Emoji] +} diff --git a/Shared/Model/Status.swift b/Shared/Model/Status.swift index 4e58c19..c703103 100644 --- a/Shared/Model/Status.swift +++ b/Shared/Model/Status.swift @@ -2,7 +2,7 @@ import Foundation -struct Status { +class Status: Codable, Identifiable { enum Visibility: String, Codable, Unknowable { case `public` case unlisted @@ -12,4 +12,96 @@ struct Status { static var unknownCase: Self { .unknown } } + + let id: String + let uri: String + let createdAt: Date + let account: Account + let content: String + let visibility: Visibility + let sensitive: Bool + let spoilerText: String + let mediaAttachments: [Attachment] + let mentions: [Mention] + let tags: [Tag] + let emojis: [Emoji] + let reblogsCount: Int + let favouritesCount: Int + let repliesCount: Int + let application: Application? + let url: URL? + let inReplyToId: String? + let inReplyToAccountId: String? + let reblog: Status? + let poll: Poll? + let card: Card? + let language: String? + let text: String? + let favourited: Bool? + let reblogged: Bool? + let muted: Bool? + let bookmarked: Bool? + let pinned: Bool? + + // Xcode-generated memberwise initializer + init( + id: String, + uri: String, + createdAt: Date, + account: Account, + content: String, + visibility: Status.Visibility, + sensitive: Bool, + spoilerText: String, + mediaAttachments: [Attachment], + mentions: [Mention], + tags: [Tag], + emojis: [Emoji], + reblogsCount: Int, + favouritesCount: Int, + repliesCount: Int, + application: Application?, + url: URL?, + inReplyToId: String?, + inReplyToAccountId: String?, + reblog: Status?, + poll: Poll?, + card: Card?, + language: String?, + text: String?, + favourited: Bool?, + reblogged: Bool?, + muted: Bool?, + bookmarked: Bool?, + pinned: Bool?) { + self.id = id + self.uri = uri + self.createdAt = createdAt + self.account = account + self.content = content + self.visibility = visibility + self.sensitive = sensitive + self.spoilerText = spoilerText + self.mediaAttachments = mediaAttachments + self.mentions = mentions + self.tags = tags + self.emojis = emojis + self.reblogsCount = reblogsCount + self.favouritesCount = favouritesCount + self.repliesCount = repliesCount + self.application = application + self.url = url + self.inReplyToId = inReplyToId + self.inReplyToAccountId = inReplyToAccountId + self.reblog = reblog + self.poll = poll + self.card = card + self.language = language + self.text = text + self.favourited = favourited + self.reblogged = reblogged + self.muted = muted + self.bookmarked = bookmarked + self.pinned = pinned + } } diff --git a/Shared/Model/Tag.swift b/Shared/Model/Tag.swift new file mode 100644 index 0000000..f15b5b0 --- /dev/null +++ b/Shared/Model/Tag.swift @@ -0,0 +1,8 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +struct Tag: Codable, Hashable { + let name: String + let url: URL +} diff --git a/Shared/Model/Timeline.swift b/Shared/Model/Timeline.swift new file mode 100644 index 0000000..0a005c5 --- /dev/null +++ b/Shared/Model/Timeline.swift @@ -0,0 +1,38 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +enum Timeline { + case home + case local + case federated + case list(MastodonList) +} + +extension Timeline { + var id: String { + switch self { + case .home: + return "home" + case .local: + return "local" + case .federated: + return "federated" + case let .list(list): + return list.id + } + } + + var endpoint: TimelinesEndpoint { + switch self { + case .home: + return .home + case .local: + return .public(local: true) + case .federated: + return .public(local: false) + case let .list(list): + return .list(id: list.id) + } + } +} diff --git a/Shared/View Models/TimelineViewModel.swift b/Shared/Model/TransientStatusCollection.swift similarity index 54% rename from Shared/View Models/TimelineViewModel.swift rename to Shared/Model/TransientStatusCollection.swift index 0072ee7..a0c3deb 100644 --- a/Shared/View Models/TimelineViewModel.swift +++ b/Shared/Model/TransientStatusCollection.swift @@ -2,6 +2,6 @@ import Foundation -class TimelineViewModel: ObservableObject { - +struct TransientStatusCollection: Codable { + let id: String } diff --git a/Shared/Networking/Mastodon API/Endpoints/TimelinesEndpoint.swift b/Shared/Networking/Mastodon API/Endpoints/TimelinesEndpoint.swift new file mode 100644 index 0000000..324e849 --- /dev/null +++ b/Shared/Networking/Mastodon API/Endpoints/TimelinesEndpoint.swift @@ -0,0 +1,42 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +enum TimelinesEndpoint { + case `public`(local: Bool) + case tag(String) + case home + case list(id: String) +} + +extension TimelinesEndpoint: MastodonEndpoint { + typealias ResultType = [Status] + + var context: [String] { + defaultContext + ["timelines"] + } + + var pathComponentsInContext: [String] { + switch self { + case .public: + return ["public"] + case let .tag(tag): + return ["tag", tag] + case .home: + return ["home"] + case let .list(id): + return ["list", id] + } + } + + var parameters: [String: Any]? { + switch self { + case let .public(local): + return ["local": local] + default: + return nil + } + } + + var method: HTTPMethod { .get } +} diff --git a/Shared/Services/IdentityService.swift b/Shared/Services/IdentityService.swift index eff9c5a..c2e3d59 100644 --- a/Shared/Services/IdentityService.swift +++ b/Shared/Services/IdentityService.swift @@ -8,6 +8,7 @@ class IdentityService { let observationErrors: AnyPublisher private let identityDatabase: IdentityDatabase + private let contentDatabase: ContentDatabase private let environment: AppEnvironment private let networkClient: MastodonClient private let secretsService: SecretsService @@ -37,6 +38,8 @@ class IdentityService { networkClient.instanceURL = identity.url networkClient.accessToken = try? secretsService.item(.accessToken) + contentDatabase = try ContentDatabase(identityID: identityID, inMemory: environment.inMemoryContent) + observation.catch { [weak self] error -> Empty in self?.observationErrorsInput.send(error) @@ -127,6 +130,10 @@ extension IdentityService { .flatMap(identityDatabase.updatePushSubscription(alerts:deviceToken:forIdentityID:)) .eraseToAnyPublisher() } + + func service(timeline: Timeline) -> StatusListService { + TimelineService(timeline: timeline, networkClient: networkClient, contentDatabase: contentDatabase) + } } private extension IdentityService { diff --git a/Shared/Services/Status List Services/StatusListService.swift b/Shared/Services/Status List Services/StatusListService.swift new file mode 100644 index 0000000..2f27667 --- /dev/null +++ b/Shared/Services/Status List Services/StatusListService.swift @@ -0,0 +1,9 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import Combine + +protocol StatusListService { + var statusSections: AnyPublisher<[[Status]], Error> { get } + func request(maxID: String?, minID: String?) -> AnyPublisher +} diff --git a/Shared/Services/Status List Services/TimelineService.swift b/Shared/Services/Status List Services/TimelineService.swift new file mode 100644 index 0000000..e0c9f6f --- /dev/null +++ b/Shared/Services/Status List Services/TimelineService.swift @@ -0,0 +1,28 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import Combine + +struct TimelineService: StatusListService { + let statusSections: AnyPublisher<[[Status]], Error> + + private let timeline: Timeline + private let networkClient: MastodonClient + private let contentDatabase: ContentDatabase + + init(timeline: Timeline, networkClient: MastodonClient, contentDatabase: ContentDatabase) { + self.timeline = timeline + self.networkClient = networkClient + self.contentDatabase = contentDatabase + statusSections = contentDatabase.statusesObservation(timeline: timeline) + .map { [$0] } + .eraseToAnyPublisher() + } + + func request(maxID: String?, minID: String?) -> AnyPublisher { + return networkClient.request(timeline.endpoint) + .map { ($0, timeline) } + .flatMap(contentDatabase.insert(statuses:collection:)) + .eraseToAnyPublisher() + } +} diff --git a/Shared/View Models/StatusesViewModel.swift b/Shared/View Models/StatusesViewModel.swift new file mode 100644 index 0000000..b6683a7 --- /dev/null +++ b/Shared/View Models/StatusesViewModel.swift @@ -0,0 +1,28 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import Combine + +class StatusesViewModel: ObservableObject { + @Published var statusSections = [[Status]]() + @Published var alertItem: AlertItem? + private let statusListService: StatusListService + private var cancellables = Set() + + init(statusListService: StatusListService) { + self.statusListService = statusListService + + statusListService.statusSections + .assignErrorsToAlertItem(to: \.alertItem, on: self) + .assign(to: &$statusSections) + } +} + +extension StatusesViewModel { + func request(maxID: String? = nil, minID: String? = nil) { + statusListService.request(maxID: maxID, minID: minID) + .assignErrorsToAlertItem(to: \.alertItem, on: self) + .sink {} + .store(in: &cancellables) + } +} diff --git a/Shared/Views/StatusesView.swift b/Shared/Views/StatusesView.swift new file mode 100644 index 0000000..92c7077 --- /dev/null +++ b/Shared/Views/StatusesView.swift @@ -0,0 +1,31 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import SwiftUI + +struct StatusesView: View { + @StateObject var viewModel: StatusesViewModel + + var body: some View { + ScrollView { + LazyVStack { + ForEach(Array(zip(viewModel.statusSections.indices, viewModel.statusSections)), + id: \.0) { _, statuses in + ForEach(statuses) { status in + Text(status.content) + Divider() + } + } + } + } + .onAppear { viewModel.request() } + .alertItem($viewModel.alertItem) + } +} + +#if DEBUG +struct StatusesView_Previews: PreviewProvider { + static var previews: some View { + StatusesView(viewModel: .development) + } +} +#endif diff --git a/Shared/Views/TimelineView.swift b/Shared/Views/TimelineView.swift deleted file mode 100644 index c310456..0000000 --- a/Shared/Views/TimelineView.swift +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright © 2020 Metabolist. All rights reserved. - -import SwiftUI - -struct TimelineView: View { - var body: some View { - Text("Time of my life") - } -} - -#if DEBUG -struct TimelineView_Previews: PreviewProvider { - static var previews: some View { - TimelineView() - } -} -#endif diff --git a/iOS/View Models/TabNavigationViewModel.swift b/iOS/View Models/TabNavigationViewModel.swift index 3594e68..ed62d61 100644 --- a/iOS/View Models/TabNavigationViewModel.swift +++ b/iOS/View Models/TabNavigationViewModel.swift @@ -6,6 +6,7 @@ import Combine class TabNavigationViewModel: ObservableObject { @Published private(set) var identity: Identity @Published private(set) var recentIdentities = [Identity]() + @Published private(set) var timelineViewModel: StatusesViewModel @Published var presentingSecondaryNavigation = false @Published var alertItem: AlertItem? var selectedTab: Tab? = .timelines @@ -16,6 +17,7 @@ class TabNavigationViewModel: ObservableObject { init(identityService: IdentityService) { self.identityService = identityService identity = identityService.identity + timelineViewModel = StatusesViewModel(statusListService: identityService.service(timeline: .home)) identityService.$identity.dropFirst().assign(to: &$identity) identityService.recentIdentitiesObservation() diff --git a/iOS/Views/TabNavigationView.swift b/iOS/Views/TabNavigationView.swift index 6c565f5..65c22bd 100644 --- a/iOS/Views/TabNavigationView.swift +++ b/iOS/Views/TabNavigationView.swift @@ -40,7 +40,7 @@ private extension TabNavigationView { func view(tab: TabNavigationViewModel.Tab) -> some View { switch tab { case .timelines: - TimelineView() + StatusesView(viewModel: viewModel.timelineViewModel) .navigationBarTitle(viewModel.identity.handle, displayMode: .inline) .navigationBarItems( leading: Button { diff --git a/macOS/View Models/SidebarNavigationViewModel.swift b/macOS/View Models/SidebarNavigationViewModel.swift index b8aba64..be6f045 100644 --- a/macOS/View Models/SidebarNavigationViewModel.swift +++ b/macOS/View Models/SidebarNavigationViewModel.swift @@ -5,6 +5,7 @@ import Combine class SidebarNavigationViewModel: ObservableObject { @Published private(set) var identity: Identity + @Published private(set) var timelineViewModel: StatusesViewModel @Published var alertItem: AlertItem? var selectedTab: Tab? = .timelines @@ -14,6 +15,7 @@ class SidebarNavigationViewModel: ObservableObject { init(identityService: IdentityService) { self.identityService = identityService identity = identityService.identity + timelineViewModel = StatusesViewModel(statusListService: identityService.service(timeline: .home)) identityService.$identity.dropFirst().assign(to: &$identity) } } diff --git a/macOS/Views/SidebarNavigationView.swift b/macOS/Views/SidebarNavigationView.swift index 7115410..68e9bf6 100644 --- a/macOS/Views/SidebarNavigationView.swift +++ b/macOS/Views/SidebarNavigationView.swift @@ -39,7 +39,7 @@ private extension SidebarNavigationView { Group { switch topLevelNavigation { case .timelines: - TimelineView() + StatusesView(viewModel: viewModel.timelineViewModel) default: Text(topLevelNavigation.title) } }