Finally got a setup working with native bindings for chrolide, sodium and whatever else for require('ssb-keys') to work

This commit is contained in:
Rogerio Chaves 2020-05-05 10:27:54 +02:00
parent 7ac7db8621
commit 5c9521090a
No known key found for this signature in database
GPG Key ID: E6AF5440509B1D94
27 changed files with 4115 additions and 132 deletions

11
ios/README.md Normal file
View File

@ -0,0 +1,11 @@
In order to build this project you'll need installed
XCode
NodeJS
Android NDK (because of some native bindings)
To compile the js to be able to run from xcode, execute:
```
npm run build:watch
```

View File

@ -1,18 +1,5 @@
console.log("Loading nodejs");
const express = require("express");
const app = express();
const port = process.env.PORT || 3000;
require("./lib/ssb");
const posts = [
{"key":"%PvT5scAQqPNiVaoYUoz5Omdx3+ds6lLEKp79Kwm02Kc=.sha256","value":{"previous":"%IU4a9V1ieToeUE2SXoqQXH0DMI0/alvxoEkGFjhoZeY=.sha256","sequence":389,"author":"@mfY4X9Gob0w2oVfFv+CpX56PfL0GZ2RNQkc51SJlMvc=.ed25519","timestamp":1588479011745,"hash":"sha256","content":{"type":"post","root":"%RRIlEQi1Mo75X5pKdJ5HOnxRU+4n2bclwIDqiLpCWf0=.sha256","branch":"%RRIlEQi1Mo75X5pKdJ5HOnxRU+4n2bclwIDqiLpCWf0=.sha256","reply":{"%RRIlEQi1Mo75X5pKdJ5HOnxRU+4n2bclwIDqiLpCWf0=.sha256":"@EaYYQo5nAQRabB9nxAn5i2uiIZ665b90Qk2U/WHNVE8=.ed25519"},"channel":null,"recps":null,"text":"This is very cool. I sometimes get mantis pods to eat pests in the garden or on our fruit trees. \n\nIt's good that you noticed that they hatched - supposedly they'll start eating each other if you leave them unattended for too long. Then I think the last one standing is the boss you have to fight to level up.\n\nIt's always so weird and cool to see them so small and in such huge numbers.","mentions":[]},"signature":"y5ixxWK/Z7R+8q7FbgImgQWKQJ+HZXqyOi9HXNPr2m8BvOHXV2zFPt/scz7Eq+1Sn1eCi7WFYK2pL+2Xk4wmCw==.sig.ed25519"},"timestamp":1588479014165,"rts":1588479011745},
{"key":"%bpmnlkq5tf5GLhV4gt8z8rZ8gsvIE55+KpRZomWug6o=.sha256","value":{"previous":"%KlYtnEt6tnVzCdjVxkye/Yy+P2Tuu7/pORBjxqvpa4M=.sha256","sequence":17384,"author":"@+oaWWDs8g73EZFUMfW37R/ULtFEjwKN/DczvdYihjbU=.ed25519","timestamp":1588466897752,"hash":"sha256","content":{"type":"post","text":"[@Powersource](@Vz6v3xKpzViiTM/GAe+hKkACZSqrErQQZgv4iqQxEn8=.ed25519)\r\n\r\nIt depends on the implementation, but I'd expect that mentions won't work unless the client specifically supports your message type.","mentions":[{"link":"@Vz6v3xKpzViiTM/GAe+hKkACZSqrErQQZgv4iqQxEn8=.ed25519","name":"Powersource"}],"root":"%jK2xn0GE975NzHfAridPvdraqDx3dM60i9UVL7JRSiE=.sha256","branch":["%1O8ZJGxOnhZ624m1nMYM57xLv3LqPDCF/q9DedaPlRc=.sha256","%nrrnKl8YJQYHWmyEjTJevOJdb7/3wcNLKoLG+z2S00c=.sha256"]},"signature":"winljHJAxDLAvRa0uc0nYQvtDh3czkHCVvzKQ+eMH+tV07EGY16z947JZ2X+djctkI6baYpaWkpezXGXc87nAg==.sig.ed25519"},"timestamp":1588466900188,"rts":1588466897752}
]
app.get("/posts", (req, res) => {
res.json(posts);
});
const expressServer = app.listen(port, () =>
console.log(`Example app listening at http://localhost:${port}`)
);
require("./lib/express");

View File

@ -0,0 +1,82 @@
const express = require("express");
const app = express();
const port = process.env.PORT || 3000;
const posts = [
{
key: "%PvT5scAQqPNiVaoYUoz5Omdx3+ds6lLEKp79Kwm02Kc=.sha256",
value: {
previous: "%IU4a9V1ieToeUE2SXoqQXH0DMI0/alvxoEkGFjhoZeY=.sha256",
sequence: 389,
author: "@mfY4X9Gob0w2oVfFv+CpX56PfL0GZ2RNQkc51SJlMvc=.ed25519",
timestamp: 1588479011745,
hash: "sha256",
content: {
type: "post",
root: "%RRIlEQi1Mo75X5pKdJ5HOnxRU+4n2bclwIDqiLpCWf0=.sha256",
branch: "%RRIlEQi1Mo75X5pKdJ5HOnxRU+4n2bclwIDqiLpCWf0=.sha256",
reply: {
"%RRIlEQi1Mo75X5pKdJ5HOnxRU+4n2bclwIDqiLpCWf0=.sha256":
"@EaYYQo5nAQRabB9nxAn5i2uiIZ665b90Qk2U/WHNVE8=.ed25519",
},
channel: null,
recps: null,
text:
"This is very cool. I sometimes get mantis pods to eat pests in the garden or on our fruit trees. \n\nIt's good that you noticed that they hatched - supposedly they'll start eating each other if you leave them unattended for too long. Then I think the last one standing is the boss you have to fight to level up.\n\nIt's always so weird and cool to see them so small and in such huge numbers.",
mentions: [],
},
signature:
"y5ixxWK/Z7R+8q7FbgImgQWKQJ+HZXqyOi9HXNPr2m8BvOHXV2zFPt/scz7Eq+1Sn1eCi7WFYK2pL+2Xk4wmCw==.sig.ed25519",
},
timestamp: 1588479014165,
rts: 1588479011745,
},
{
key: "%bpmnlkq5tf5GLhV4gt8z8rZ8gsvIE55+KpRZomWug6o=.sha256",
value: {
previous: "%KlYtnEt6tnVzCdjVxkye/Yy+P2Tuu7/pORBjxqvpa4M=.sha256",
sequence: 17384,
author: "@+oaWWDs8g73EZFUMfW37R/ULtFEjwKN/DczvdYihjbU=.ed25519",
timestamp: 1588466897752,
hash: "sha256",
content: {
type: "post",
text:
"[@Powersource](@Vz6v3xKpzViiTM/GAe+hKkACZSqrErQQZgv4iqQxEn8=.ed25519)\r\n\r\nIt depends on the implementation, but I'd expect that mentions won't work unless the client specifically supports your message type.",
mentions: [
{
link: "@Vz6v3xKpzViiTM/GAe+hKkACZSqrErQQZgv4iqQxEn8=.ed25519",
name: "Powersource",
},
],
root: "%jK2xn0GE975NzHfAridPvdraqDx3dM60i9UVL7JRSiE=.sha256",
branch: [
"%1O8ZJGxOnhZ624m1nMYM57xLv3LqPDCF/q9DedaPlRc=.sha256",
"%nrrnKl8YJQYHWmyEjTJevOJdb7/3wcNLKoLG+z2S00c=.sha256",
],
},
signature:
"winljHJAxDLAvRa0uc0nYQvtDh3czkHCVvzKQ+eMH+tV07EGY16z947JZ2X+djctkI6baYpaWkpezXGXc87nAg==.sig.ed25519",
},
timestamp: 1588466900188,
rts: 1588466897752,
},
];
app.get("/user", (req, res) => {
res.json({
profile: {
id: "@PvT5scAQqPNiVaoYUoz5Omdx3",
name: "Jose",
image: "http://pudim.com.br/pudim.jpg",
},
});
});
app.get("/posts", (req, res) => {
res.json(posts);
});
app.listen(port, () =>
console.log(`Example app listening at http://localhost:${port}`)
);

View File

@ -0,0 +1,145 @@
// Monkeypatched to add getGraph
"use strict";
var LayeredGraph = require("layered-graph");
var pull = require("pull-stream");
var isFeed = require("ssb-ref").isFeed;
// friends plugin
// methods to analyze the social graph
// maintains a 'follow' and 'flag' graph
exports.name = "friends";
exports.version = "1.0.0";
exports.manifest = {
hopStream: "source",
onEdge: "sync",
isFollowing: "async",
isBlocking: "async",
getGraph: "async",
hops: "async",
help: "sync",
// createLayer: 'sync', // not exposed over RPC as returns a function
get: "async", // legacy
createFriendStream: "source", // legacy
stream: "source", // legacy
};
//mdm.manifest(apidoc)
exports.init = function (sbot, config) {
var max =
(config.friends && config.friends.hops) ||
(config.replicate && config.replicate.hops) ||
3;
var layered = LayeredGraph({ max: max, start: sbot.id });
function getGraph(cb) {
layered.onReady(function () {
var g = layered.getGraph();
cb(null, g);
});
}
function isFollowing(opts, cb) {
layered.onReady(function () {
var g = layered.getGraph();
cb(null, g[opts.source] ? g[opts.source][opts.dest] >= 0 : false);
});
}
function isBlocking(opts, cb) {
layered.onReady(function () {
var g = layered.getGraph();
cb(null, Math.round(g[opts.source] && g[opts.source][opts.dest]) == -1);
});
}
//opinion: do not authorize peers blocked by this node.
sbot.auth.hook(function (fn, args) {
var self = this;
isBlocking({ source: sbot.id, dest: args[0] }, function (err, blocked) {
if (blocked) args[1](new Error("client is blocked"));
else fn.apply(self, args);
});
});
if (!sbot.replicate)
throw new Error("ssb-friends expects a replicate plugin to be available");
// opinion: replicate with everyone within max hops (max passed to layered above ^)
pull(
layered.hopStream({ live: true, old: true }),
pull.drain(function (data) {
if (data.sync) return;
for (var k in data) {
sbot.replicate.request(k, data[k] >= 0);
}
})
);
require("ssb-friends/contacts")(sbot, layered.createLayer, config);
var legacy = require("ssb-friends/legacy")(layered);
//opinion: pass the blocks to replicate.block
setImmediate(function () {
var block =
(sbot.replicate && sbot.replicate.block) || (sbot.ebt && sbot.ebt.block);
if (block) {
function handleBlockUnlock(from, to, value) {
if (value === false) block(from, to, true);
else block(from, to, false);
}
pull(
legacy.stream({ live: true }),
pull.drain(function (contacts) {
if (!contacts) return;
if (isFeed(contacts.from) && isFeed(contacts.to)) {
// live data
handleBlockUnlock(contacts.from, contacts.to, contacts.value);
} else {
// initial data
for (var from in contacts) {
var relations = contacts[from];
for (var to in relations)
handleBlockUnlock(from, to, relations[to]);
}
}
})
);
}
});
return {
hopStream: layered.hopStream,
onEdge: layered.onEdge,
isFollowing: isFollowing,
isBlocking: isBlocking,
getGraph: getGraph,
// expose createLayer, so that other plugins may express relationships
createLayer: layered.createLayer,
// legacy, debugging
hops: function (opts, cb) {
layered.onReady(function () {
if (isFunction(opts)) (cb = opts), (opts = {});
cb(null, layered.getHops(opts));
});
},
help: function () {
return require("ssb-friends/help");
},
// legacy
get: legacy.get,
createFriendStream: legacy.createFriendStream,
stream: legacy.stream,
};
};
// helpers
function isFunction(f) {
return "function" === typeof f;
}

View File

@ -0,0 +1,85 @@
// Differently from ssb-identities, this plugin only keeps keys in memory, as we don't want to save them on the server
const ssbKeys = require("ssb-keys");
const { create } = require("ssb-validate");
exports.name = "identities";
exports.version = "1.0.0";
exports.manifest = {
create: "sync",
addUnboxer: "sync",
publishAs: "async",
createNewKey: "sync",
};
let unboxersAdded = [];
let locks = {};
const toTarget = (t) => (typeof t == "object" ? t && t.link : t);
const addUnboxer = (ssb) => (key) => {
if (unboxersAdded.includes(key.id)) return;
ssb.addUnboxer({
key: (content) => {
const unboxKey = ssbKeys.unboxKey(content, key);
if (unboxKey) return unboxKey;
},
value: (content, key) => {
return ssbKeys.unboxBody(content, key);
},
});
unboxersAdded.push(key.id);
};
const publishAs = (ssb, config) => ({ key, private, content }, cb) => {
const id = key.id;
if (locks[id]) throw new Error("already writing");
const recps = [].concat(content.recps).map(toTarget);
if (content.recps && !private) {
return new Error("recps set, but private not set");
} else if (!content.recps && private) {
return new Error("private set, but content.recps not set");
} else if (!!content.recps && private) {
if (!Array.isArray(content.recps) || !~recps.indexOf(id))
return new Error(
"content.recps must be an array containing publisher id:" +
id +
" was:" +
JSON.stringify(recps) +
" indexOf:" +
recps.indexOf(id)
);
content = ssbKeys.box(content, recps);
}
locks[id] = true;
ssb.getLatest(id, (_err, data) => {
const state = data
? {
id: data.key,
sequence: data.value.sequence,
timestamp: data.value.timestamp,
queue: [],
}
: { id: null, sequence: null, timestamp: null, queue: [] };
ssb.add(
create(state, key, config.caps && config.caps.sign, content, Date.now()),
(err, a, b) => {
delete locks[id];
cb(err, a, b);
}
);
});
};
exports.init = function (ssb, config) {
return {
addUnboxer: addUnboxer(ssb),
publishAs: publishAs(ssb, config),
createNewKey: ssbKeys.generate,
};
};

696
ios/backend/lib/queries.js Normal file
View File

@ -0,0 +1,696 @@
const pull = require("pull-stream");
const cat = require("pull-cat");
const debugPosts = require("debug")("queries:posts"),
debugMessages = require("debug")("queries:messages"),
debugFriends = require("debug")("queries:friends"),
debugFriendshipStatus = require("debug")("queries:friendship_status"),
debugSearch = require("debug")("queries:search"),
debugProfile = require("debug")("queries:profile"),
debugCommunities = require("debug")("queries:communities"),
debugCommunityMembers = require("debug")("queries:communityMembers"),
debugCommunityPosts = require("debug")("queries:communityPosts"),
debugCommunityIsMember = require("debug")("queries:communityIsMember"),
debugCommunityProfileCommunities = require("debug")(
"queries:communityProfileCommunities"
);
const paramap = require("pull-paramap");
const { promisePull, mapValues } = require("./utils");
const ssb = require("./ssb-client");
const latestOwnerValue = ({ key, dest }) => {
return promisePull(
ssb.client().query.read({
reverse: true,
query: [
{
$filter: {
value: {
author: dest,
content: { type: "about", about: dest },
},
},
},
],
}),
pull.filter((msg) => {
return (
msg.value.content &&
key in msg.value.content &&
!(msg.value.content[key] && msg.value.content[key].remove)
);
}),
pull.take(1)
).then(([entry]) => {
if (entry) {
return entry.value.content[key];
}
return ssb.client().about.latestValue({ key, dest });
});
};
const mapProfiles = (data, callback) =>
getProfile(data.value.author)
.then((author) => {
data.value.authorProfile = author;
callback(null, data);
})
.catch((err) => callback(err, null));
const getPosts = async (profile) => {
debugPosts("Fetching");
const posts = await promisePull(
// @ts-ignore
cat([
ssb.client().query.read({
reverse: true,
query: [
{
$filter: {
value: {
private: { $not: true },
content: {
root: profile.id,
},
},
},
},
],
limit: 100,
}),
ssb.client().query.read({
reverse: true,
query: [
{
$filter: {
value: {
author: profile.id,
private: { $not: true },
content: {
type: "post",
root: { $not: true },
channel: { $not: true },
},
},
},
},
],
limit: 100,
}),
]),
pull.filter((msg) => msg.value.content.type == "post"),
paramap(mapProfiles)
);
debugPosts("Done");
return mapValues(posts);
};
const getSecretMessages = async (profile) => {
debugMessages("Fetching");
const messagesPromise = promisePull(
// @ts-ignore
cat([
ssb.client().private.read({
reverse: true,
limit: 100,
}),
]),
pull.filter(
(msg) =>
msg.value.content.type == "post" &&
msg.value.content.recps &&
msg.value.content.recps.includes(profile.id)
)
);
const deletedPromise = promisePull(
ssb.client().query.read({
reverse: true,
query: [
{
$filter: {
value: {
author: profile.id,
content: {
type: "delete",
},
},
},
},
],
})
).then(Object.values);
const [messages, deleted] = await Promise.all([
messagesPromise,
deletedPromise,
]);
const deletedIds = deleted.map((x) => x.value.content.dest);
const messagesByAuthor = {};
for (const message of messages) {
if (message.value.author == profile.id) {
for (const recp of message.value.content.recps) {
if (recp == profile.id) continue;
if (!messagesByAuthor[recp]) {
messagesByAuthor[recp] = {
author: recp,
messages: [],
};
}
}
continue;
}
const author = message.value.author;
if (!messagesByAuthor[author]) {
messagesByAuthor[author] = {
author: message.value.author,
messages: [],
};
}
if (!deletedIds.includes(message.key))
messagesByAuthor[author].messages.push(message);
}
const profilesList = await Promise.all(
Object.keys(messagesByAuthor).map((id) => getProfile(id))
);
const profilesHash = profilesList.reduce((hash, profile) => {
hash[profile.id] = profile;
return hash;
}, {});
const chatList = Object.values(messagesByAuthor).map((m) => {
m.authorProfile = profilesHash[m.author];
return m;
});
debugMessages("Done");
return chatList;
};
const search = async (search) => {
debugSearch("Fetching");
// https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
const normalizedSearch = search
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "");
const safelyEscapedSearch = normalizedSearch.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&"
);
const loosenSpacesSearch = safelyEscapedSearch.replace(" ", ".*");
const searchRegex = new RegExp(`.*${loosenSpacesSearch}.*`, "i");
const peoplePromise = promisePull(
ssb.client().query.read({
reverse: true,
query: [
{
$filter: {
value: {
content: {
type: "about",
name: { $is: "string" },
},
},
},
},
],
}),
pull.filter((msg) => {
if (!msg.value.content) return;
const normalizedName = msg.value.content.name
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "");
return searchRegex.exec(normalizedName);
}),
paramap(mapProfiles)
);
const communitiesPostsPromise = promisePull(
ssb.client().query.read({
reverse: true,
query: [
{
$filter: {
value: {
private: { $not: true },
content: {
type: "post",
channel: { $truthy: true },
},
},
},
},
],
limit: 3000,
})
);
const [people, communitiesPosts] = await Promise.all([
peoplePromise,
communitiesPostsPromise,
]);
const communities = Array.from(
new Set(communitiesPosts.map((p) => p.value.content.channel))
).filter((name) => {
const normalizedName = name
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "");
return searchRegex.exec(normalizedName);
});
debugSearch("Done");
return { people: Object.values(mapValues(people)), communities };
};
const getFriends = async (profile) => {
debugFriends("Fetching");
let graph = await ssb.client().friends.getGraph();
let connections = {};
for (let key in graph) {
let isFollowing = graph[profile.id] && graph[profile.id][key] > 0;
let isFollowingBack = graph[key] && graph[key][profile.id] > 0;
if (isFollowing && isFollowingBack) {
connections[key] = "friends";
} else if (isFollowing && !isFollowingBack) {
connections[key] = "requestsSent";
} else if (!isFollowing && isFollowingBack) {
if (!graph[profile.id] || graph[profile.id][key] === undefined)
connections[key] = "requestsReceived";
}
}
const profilesList = await Promise.all(
Object.keys(connections).map((id) => getProfile(id))
);
const profilesHash = profilesList.reduce((hash, profile) => {
hash[profile.id] = profile;
return hash;
}, {});
let result = {
friends: [],
requestsSent: [],
requestsReceived: [],
};
for (let key in connections) {
result[connections[key]].push(profilesHash[key]);
}
debugFriends("Done");
return result;
};
const getFriendshipStatus = async (source, dest) => {
debugFriendshipStatus("Fetching");
let requestRejectionsPromise = promisePull(
ssb.client().query.read({
reverse: true,
query: [
{
$filter: {
value: {
author: source,
content: {
type: "contact",
following: false,
},
},
},
},
],
limit: 100,
})
).then(mapValues);
const [isFollowing, isFollowingBack, requestRejections] = await Promise.all([
ssb.client().friends.isFollowing({ source: source, dest: dest }),
ssb.client().friends.isFollowing({ source: dest, dest: source }),
requestRejectionsPromise.then((x) => x.map((y) => y.content.contact)),
]);
let status = "no_relation";
if (isFollowing && isFollowingBack) {
status = "friends";
} else if (isFollowing && !isFollowingBack) {
status = "request_sent";
} else if (!isFollowing && isFollowingBack) {
if (requestRejections.includes(dest)) {
status = "request_rejected";
} else {
status = "request_received";
}
}
debugFriendshipStatus("Done");
return status;
};
const getAllEntries = (query) => {
let queries = [];
if (query.author) {
queries.push({ $filter: { value: { author: query.author } } });
}
if (query.type) {
queries.push({ $filter: { value: { content: { type: query.type } } } });
}
const queryOpts = queries.length > 0 ? { query: queries } : {};
return promisePull(
ssb.client().query.read({
reverse: true,
limit: 1000,
...queryOpts,
})
);
};
let profileCache = {};
const getProfile = async (id) => {
if (profileCache[id]) return profileCache[id];
let getKey = (key) => latestOwnerValue({ key, dest: id });
let [name, image, description] = await Promise.all([
getKey("name"),
getKey("image"),
getKey("description"),
]).catch((err) => {
console.error("Could not retrieve profile for", id, err);
});
let profile = { id, name, image, description };
profileCache[id] = profile;
return profile;
};
const progress = (callback) => {
pull(
ssb.client().replicate.changes(),
pull.drain(callback, (err) => {
console.error("Progress drain error", err);
})
);
};
const autofollow = async (id) => {
const isFollowing = await ssb.client().friends.isFollowing({
source: ssb.client().id,
dest: id,
});
if (!isFollowing) {
await ssb.client().publish({
type: "contact",
contact: id,
following: true,
autofollow: true,
});
}
};
const getCommunities = async () => {
debugCommunities("Fetching");
const communitiesPosts = await promisePull(
ssb.client().query.read({
reverse: true,
query: [
{
$filter: {
value: {
private: { $not: true },
content: {
type: "post",
channel: { $truthy: true },
},
},
},
},
],
limit: 1000,
})
);
const communities = Array.from(
new Set(communitiesPosts.map((p) => p.value.content.channel))
);
debugCommunities("Done");
return communities;
};
const isMember = async (id, channel) => {
debugCommunityIsMember("Fetching");
const [lastSubscription] = await promisePull(
ssb.client().query.read({
reverse: true,
limit: 1,
query: [
{
$filter: {
value: {
author: id,
content: {
type: "channel",
channel: channel,
},
},
},
},
],
})
);
debugCommunityIsMember("Done");
return lastSubscription && lastSubscription.value.content.subscribed;
};
const getCommunityMembers = async (name) => {
debugCommunityMembers("Fetching");
const communityMembers = await promisePull(
ssb.client().query.read({
reverse: true,
query: [
{
$filter: {
value: {
content: {
type: "channel",
channel: name,
},
},
},
},
],
limit: 100,
}),
paramap(mapProfiles)
);
const dedupMembers = {};
for (const member of communityMembers) {
const author = member.value.author;
if (dedupMembers[author]) continue;
dedupMembers[author] = member;
}
const onlySubscribedMembers = Object.values(dedupMembers).filter(
(x) => x.value.content.subscribed
);
const memberProfiles = onlySubscribedMembers.map(
(x) => x.value.authorProfile
);
debugCommunityMembers("Done");
return memberProfiles;
};
const getProfileCommunities = async (id) => {
debugCommunityProfileCommunities("Fetching");
const subscriptions = await promisePull(
ssb.client().query.read({
reverse: true,
query: [
{
$filter: {
value: {
author: id,
content: {
type: "channel",
},
},
},
},
],
})
);
const dedupSubscriptions = {};
for (const subscription of subscriptions) {
const channel = subscription.value.content.channel;
if (dedupSubscriptions[channel]) continue;
dedupSubscriptions[channel] = subscription;
}
const onlyActiveSubscriptions = Object.values(dedupSubscriptions).filter(
(x) => x.value.content.subscribed
);
const channelNames = onlyActiveSubscriptions.map(
(x) => x.value.content.channel
);
debugCommunityProfileCommunities("Done");
return channelNames;
};
const getPostWithReplies = async (channel, key) => {
debugCommunityPosts("Fetching");
const postWithReplies = await promisePull(
// @ts-ignore
cat([
ssb.client().query.read({
reverse: false,
limit: 1,
query: [
{
$filter: {
key: key,
value: {
content: {
type: "post",
channel: channel,
},
},
},
},
],
}),
ssb.client().query.read({
reverse: false,
limit: 50,
query: [
{
$filter: {
value: {
content: {
root: key,
channel: channel,
},
},
},
},
],
}),
ssb.client().query.read({
reverse: false,
limit: 50,
query: [
{
$filter: {
value: {
content: {
reply: { $prefix: [key] },
channel: channel,
},
},
},
},
],
}),
]),
paramap(mapProfiles)
);
debugCommunityPosts("Done");
return postWithReplies;
};
const getCommunityPosts = async (name) => {
debugCommunityPosts("Fetching");
const communityPosts = await promisePull(
ssb.client().query.read({
reverse: true,
query: [
{
$filter: {
value: {
content: {
type: "post",
channel: name,
},
},
},
},
],
limit: 1000,
}),
paramap(mapProfiles)
);
let communityPostsByKey = {};
let replies = [];
let rootKey = (post) => {
let replyKey =
post.value.content.reply && Object.keys(post.value.content.reply)[0];
return replyKey || post.value.content.root;
};
for (let post of communityPosts) {
if (rootKey(post)) {
replies.push(post);
} else {
post.value.replies = [];
communityPostsByKey[post.key] = post;
}
}
for (let reply of replies) {
let root = communityPostsByKey[rootKey(reply)];
if (root) root.value.replies.push(reply);
}
debugCommunityPosts("Done");
return Object.values(communityPostsByKey);
};
if (!global.clearProfileInterval) {
global.clearProfileInterval = setInterval(() => {
debugProfile("Clearing profile cache");
profileCache = {};
}, 5 * 60 * 1000);
}
module.exports = {
mapProfiles,
getPosts,
search,
getFriends,
getAllEntries,
getProfile,
getSecretMessages,
profileCache,
getFriendshipStatus,
getCommunities,
getCommunityMembers,
getCommunityPosts,
getPostWithReplies,
progress,
autofollow,
isMember,
getProfileCommunities,
};

View File

@ -0,0 +1,53 @@
const Client = require("ssb-client");
const ssbKeys = require("ssb-keys");
const ssbConfig = require("./ssb-config");
const queries = require("./queries");
const debug = require("debug")("express");
const { ssbFolder } = require("./utils");
const fetch = require("node-fetch").default;
let ssbClient;
let syncing = false;
const mode = process.env.MODE || "standalone";
const ssbSecret = ssbKeys.loadOrCreateSync(`${ssbFolder()}/secret`);
const connectClient = (ssbSecret) => {
Client(ssbSecret, ssbConfig, async (err, server) => {
if (err) throw err;
ssbClient = server;
queries.progress(({ rate, feeds, incompleteFeeds, progress, total }) => {
if (incompleteFeeds > 0) {
if (!syncing) debug("syncing");
syncing = true;
} else {
syncing = false;
}
});
console.log("SSB Client ready");
if (mode == "standalone") addFirstPub();
});
};
const addFirstPub = async () => {
const peers = await ssbClient.gossip.peers();
if (peers.length == 0) {
console.log("No pubs found, adding pub.feedless.social as a first pub");
try {
const response = await fetch("https://feedless.social/pub_invite");
const { invite } = await response.json();
await ssbClient.invite.accept(invite);
} catch (e) {
console.error("Could add feedless pub", e);
}
}
};
module.exports.client = () => ssbClient;
module.exports.isSyncing = () => syncing;
module.exports.reconnectWith = connectClient;
connectClient(ssbSecret);

View File

@ -0,0 +1,20 @@
const configInject = require("ssb-config/inject");
module.exports = configInject(process.env.CONFIG_FOLDER || "ssb", {
connections: {
incoming: {
net: [
{
scope: "public",
host: "0.0.0.0",
external: "pub.feedless.social",
transform: "shs",
port: process.env.SSB_PORT || 8008,
},
],
},
outgoing: {
net: [{ transform: "shs" }],
},
},
});

53
ios/backend/lib/ssb.js Normal file
View File

@ -0,0 +1,53 @@
try {
const fs = require("fs");
const path = require("path");
const { writeKey, ssbFolder } = require("./utils");
const SecretStack = require("secret-stack");
const mkdirp = require("mkdirp");
// const ssbKeys = require("ssb-keys");
console.log("ssbFolder", ssbFolder());
const folderExists = fs.existsSync(ssbFolder());
if (!folderExists) mkdirp.sync(ssbFolder());
const keysPath = path.join(ssbFolder(), "/secret");
} catch (e) {
console.log("error", e);
}
// const keys = ssbKeys.loadOrCreateSync(keysPath);
// // Need to use secret-stack directly instead of ssb-server here otherwise is not compatible with patchwork .ssb folder
// const Server = require("secret-stack")();
// .use(require("ssb-db"))
// .use(require("ssb-master"));
// .use(require("ssb-gossip"))
// .use(require("ssb-replicate"))
// .use(require("ssb-backlinks"))
// .use(require("ssb-about"))
// .use(require("ssb-contacts"))
// .use(require("ssb-invite"))
// .use(require("./monkeypatch/ssb-friends"))
// .use(require("ssb-query"))
// .use(require("ssb-device-address"))
// .use(require("./plugins/memory-identities"))
// .use(require("ssb-blobs"))
// .use(require("ssb-private"));
// const config = require("./ssb-config");
// const server = Server(config);
// console.log("SSB server started at", config.port);
// // save an updated list of methods this server has made public
// // in a location that ssb-client will know to check
// const manifest = server.getManifest();
// fs.writeFileSync(
// path.join(config.path, "manifest.json"), // ~/.ssb/manifest.json
// JSON.stringify(manifest)
// );
// // SSB server automatically creates a secret key, but we want the user flow where they choose to create a key or use an existing one
// const mode = process.env.MODE || "standalone";
// if (mode == "standalone" && !secretExists) {
// fs.writeFileSync(`${ssbFolder()}/logged-out`, "");
// }

79
ios/backend/lib/utils.js Normal file
View File

@ -0,0 +1,79 @@
const fs = require("fs");
const pull = require("pull-stream");
module.exports.asyncRouter = (app) => {
const debug = require("debug")("router");
let wrapper = (method, path, opts, fn) => async (req, res, next) => {
if (typeof opts == "function") fn = opts;
if (!opts.public && !req.context.profile) {
if (method == "POST") {
res.status(401);
return res.send("You are not logged in");
}
return res.redirect("/");
}
req.context.path = path;
try {
debug(`${method} ${path}`);
await fn(req, res);
} catch (e) {
next(e);
}
};
return {
get: (path, fn, opts) => {
app.get(path, wrapper("GET", path, fn, opts));
},
post: (path, fn, opts) => {
debug(`POST ${path}`);
app.post(path, wrapper("POST", path, fn, opts));
},
};
};
const ssbFolder = () => {
let homeFolder =
process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
return `${process.argv[2] || homeFolder + "/.ssb"}`;
};
module.exports.ssbFolder = ssbFolder;
module.exports.writeKey = (key, path) => {
let secretPath = `${ssbFolder()}${path}`;
// Same options ssb-keys use
try {
fs.mkdirSync(ssbFolder(), { recursive: true });
} catch (e) {}
fs.writeFileSync(secretPath, key, { mode: 0x100, flag: "wx" });
};
// From ssb-keys
module.exports.reconstructKeys = (keyfile) => {
var privateKey = keyfile
.replace(/\s*\#[^\n]*/g, "")
.split("\n")
.filter((x) => x)
.join("");
var keys = JSON.parse(privateKey);
const hasSigil = (x) => /^(@|%|&)/.test(x);
if (!hasSigil(keys.id)) keys.id = "@" + keys.public;
return keys;
};
module.exports.promisePull = (...streams) =>
new Promise((resolve, reject) => {
pull(
...streams,
pull.collect((err, msgs) => {
if (err) return reject(err);
return resolve(msgs);
})
);
});
module.exports.mapValues = (x) => x.map((y) => y.value);

File diff suppressed because it is too large Load Diff

View File

@ -4,14 +4,41 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "./tools/build-backend.js"
"start": "SSB_DIR=~/.ssb node index.js",
"build": "./tools/build-backend.js",
"build:watch": "watch ./tools/build-backend.js lib/"
},
"author": "",
"license": "ISC",
"dependencies": {
"bindings-noderify-nodejs-mobile": "10.3.0",
"debug": "^4.1.1",
"express": "^4.17.1",
"ora": "^4.0.4"
"layered-graph": "^1.1.3",
"leveldown-nodejs-mobile": "5.1.1-3",
"ora": "^4.0.4",
"pull-cat": "^1.1.11",
"pull-paramap": "^1.2.2",
"pull-stream": "^3.6.14",
"secret-stack": "6.3.1",
"sodium-chloride-native-nodejs-mobile": "1.1.0",
"ssb-about": "^2.0.1",
"ssb-backlinks": "^1.0.0",
"ssb-blobs": "^1.2.2",
"ssb-config": "^3.4.4",
"ssb-contacts": "0.0.2",
"ssb-db": "^19.4.0",
"ssb-device-address": "^1.1.6",
"ssb-friends": "^4.1.4",
"ssb-gossip": "^1.1.1",
"ssb-invite": "^2.1.4",
"ssb-keys": "7.2.0",
"ssb-master": "^1.0.3",
"ssb-private": "^0.2.3",
"ssb-query": "^2.4.3",
"ssb-ref": "^2.13.9",
"ssb-replicate": "^1.3.2",
"watch": "^1.0.2"
},
"devDependencies": {
"noderify": "4.3.0"

View File

@ -20,11 +20,6 @@ function onFailure() {
# remove unused packages such as sodium-browserify etc
# leveldown: newer versions of leveldown are intentionally ignoring
# nodejs-mobile support, so we run an older version
# utp-native: we want to compile for nodejs-mobile instead of using prebuilds
# node-extend: can't remember why we need to replace it, build seemed to fail
# non-private-ip: we use a "better" fork of this package
# multiserver net plugin: we're fixing a corner case bug with error recovery
# rn-bridge: this is not an npm package, it's just a nodejs-mobile shortcut
# bl: we didn't use it, and bl@0.8.x has security vulnerabilities
# bufferutil: because we want nodejs-mobile to load its native bindings
# supports-color: optional dependency within package `debug`
@ -34,20 +29,9 @@ $(npm bin)/noderify \
--replace.bindings=bindings-noderify-nodejs-mobile \
--replace.chloride=sodium-chloride-native-nodejs-mobile \
--replace.leveldown=leveldown-nodejs-mobile \
--replace.utp-native=utp-native-nodejs-mobile \
--replace.node-extend=xtend \
--replace.non-private-ip=non-private-ip-android \
--replace.multiserver/plugins/net=staltz-multiserver/plugins/net \
--filter=rn-bridge \
--filter=bl \
--filter=bufferutil \
--filter=supports-color \
--filter=utf-8-validate \
--filter=bip39/src/wordlists/chinese_simplified.json \
--filter=bip39/src/wordlists/chinese_traditional.json \
--filter=bip39/src/wordlists/french.json \
--filter=bip39/src/wordlists/italian.json \
--filter=bip39/src/wordlists/japanese.json \
--filter=bip39/src/wordlists/korean.json \
--filter=bip39/src/wordlists/spanish.json \
backend/index.js > out/index.js;
index.js > out/index.js;

View File

@ -30,3 +30,5 @@ find ./node_modules \
-name "electron-napi.node" \
\) \
-print0 | xargs -0 rm -rf # delete everything in the list
# Android builds
rm -rf ./node_modules/sodium-native-nodejs-mobile/libsodium/android-*

View File

@ -36,17 +36,10 @@ async function runAndReport(label, task) {
}
(async function () {
if (process.env.NODE_ENV == "production") {
await runAndReport(
"Install backend node modules",
exec("npm install --no-optional")
);
await runAndReport(
"Remove unused files meant for macOS or Windows or Electron",
exec("./tools/backend/remove-unused-files.sh")
);
}
await runAndReport(
"Remove unused files meant for Android or Electron",
exec("./tools/backend/remove-unused-files.sh")
);
await runAndReport(
"Bundle and minify backend JS into one file",

View File

@ -17,8 +17,10 @@
902D04812458AAAA007CFE56 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 902D047F2458AAAA007CFE56 /* LaunchScreen.storyboard */; };
902D048C2458AAAA007CFE56 /* feedlessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902D048B2458AAAA007CFE56 /* feedlessTests.swift */; };
902D04972458AAAA007CFE56 /* feedlessUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902D04962458AAAA007CFE56 /* feedlessUITests.swift */; };
90B81B90245ECB25005C5D31 /* NodeMobile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90B81B8F245ECB25005C5D31 /* NodeMobile.framework */; };
90B81B91245ECB25005C5D31 /* NodeMobile.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 90B81B8F245ECB25005C5D31 /* NodeMobile.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9053055B245F7C23006C054D /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9053055A245F7C23006C054D /* Utils.swift */; };
9053055F245FE975006C054D /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9053055E245FE975006C054D /* Types.swift */; };
90A1A5312460D24600D00245 /* NodeMobile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90A1A52D2460D16500D00245 /* NodeMobile.framework */; };
90A1A5322460D24600D00245 /* NodeMobile.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 90A1A52D2460D16500D00245 /* NodeMobile.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
90B81B96245ECC00005C5D31 /* NodeRunner.mm in Sources */ = {isa = PBXBuildFile; fileRef = 90B81B95245ECC00005C5D31 /* NodeRunner.mm */; };
90B81B98245F1699005C5D31 /* backend in Resources */ = {isa = PBXBuildFile; fileRef = 90B81B97245F1698005C5D31 /* backend */; };
/* End PBXBuildFile section */
@ -41,13 +43,13 @@
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
90B81B92245ECB25005C5D31 /* Embed Frameworks */ = {
90A1A5332460D24600D00245 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
90B81B91245ECB25005C5D31 /* NodeMobile.framework in Embed Frameworks */,
90A1A5322460D24600D00245 /* NodeMobile.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@ -71,7 +73,9 @@
902D04922458AAAA007CFE56 /* feedlessUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = feedlessUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
902D04962458AAAA007CFE56 /* feedlessUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = feedlessUITests.swift; sourceTree = "<group>"; };
902D04982458AAAA007CFE56 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
90B81B8F245ECB25005C5D31 /* NodeMobile.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NodeMobile.framework; path = libnode/NodeMobile.framework; sourceTree = "<group>"; };
9053055A245F7C23006C054D /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
9053055E245FE975006C054D /* Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = "<group>"; };
90A1A52D2460D16500D00245 /* NodeMobile.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NodeMobile.framework; path = libnode/NodeMobile.framework; sourceTree = "<group>"; };
90B81B93245ECBBB005C5D31 /* NodeRunner.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NodeRunner.h; sourceTree = "<group>"; };
90B81B94245ECBFF005C5D31 /* feedless-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "feedless-Bridging-Header.h"; sourceTree = "<group>"; };
90B81B95245ECC00005C5D31 /* NodeRunner.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = NodeRunner.mm; sourceTree = "<group>"; };
@ -83,7 +87,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
90B81B90245ECB25005C5D31 /* NodeMobile.framework in Frameworks */,
90A1A5312460D24600D00245 /* NodeMobile.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -149,6 +153,8 @@
90B81B93245ECBBB005C5D31 /* NodeRunner.h */,
90B81B95245ECC00005C5D31 /* NodeRunner.mm */,
90B81B94245ECBFF005C5D31 /* feedless-Bridging-Header.h */,
9053055A245F7C23006C054D /* Utils.swift */,
9053055E245FE975006C054D /* Types.swift */,
);
path = feedless;
sourceTree = "<group>";
@ -182,7 +188,7 @@
90B81B8E245ECB25005C5D31 /* Frameworks */ = {
isa = PBXGroup;
children = (
90B81B8F245ECB25005C5D31 /* NodeMobile.framework */,
90A1A52D2460D16500D00245 /* NodeMobile.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -197,7 +203,7 @@
902D046D2458AAA7007CFE56 /* Sources */,
902D046E2458AAA7007CFE56 /* Frameworks */,
902D046F2458AAA7007CFE56 /* Resources */,
90B81B92245ECB25005C5D31 /* Embed Frameworks */,
90A1A5332460D24600D00245 /* Embed Frameworks */,
);
buildRules = (
);
@ -324,8 +330,10 @@
902D04752458AAA7007CFE56 /* AppDelegate.swift in Sources */,
90B81B96245ECC00005C5D31 /* NodeRunner.mm in Sources */,
900BE29A2458B05800C77595 /* Login.swift in Sources */,
9053055B245F7C23006C054D /* Utils.swift in Sources */,
902D04772458AAA7007CFE56 /* SceneDelegate.swift in Sources */,
900BE2972458AEEC00C77595 /* Index.swift in Sources */,
9053055F245FE975006C054D /* Types.swift in Sources */,
902D04792458AAA7007CFE56 /* Wall.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -495,6 +503,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"feedless/Preview Content\"";
DEVELOPMENT_TEAM = 8FY84BDRUF;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -522,6 +531,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"feedless/Preview Content\"";
DEVELOPMENT_TEAM = 8FY84BDRUF;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -535,6 +545,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.rogeriochaves.feedless;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "feedless/feedless-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};

View File

@ -14,8 +14,8 @@
filePath = "feedless/screens/Wall.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "33"
endingLineNumber = "33"
startingLineNumber = "19"
endingLineNumber = "19"
landmarkName = "init()"
landmarkType = "7">
</BreakpointContent>

View File

@ -0,0 +1,7 @@
//
// Queries.swift
// feedless
//
// Created by Rogerio Chaves on 04/05/20.
// Copyright © 2020 Rogerio Chaves. All rights reserved.
//

View File

@ -21,22 +21,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
let jsFile = Bundle.main.path(forResource: "backend/index.js", ofType: nil)!
let jsFile = Bundle.main.path(forResource: "backend/out/index.js", ofType: nil)!
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
print("documentsPath", documentsPath)
DispatchQueue.global(qos: .background).async {
NodeRunner.startEngine(withArguments: ["node", jsFile])
NodeRunner.startEngine(withArguments: ["node", jsFile, documentsPath])
}
//
// do {
// context.evaluateScript(try String(contentsOf: url), withSourceURL: url)
// } catch _ {
// fatalError("could not evaluate index.js")
// }
//
// let main = context.objectForKeyedSubscript("main")
// print("aaaaaaaaaaaaaaaaaaaa", main?.call(withArguments: []))
// Create the SwiftUI view that provides the window contents.
let contentView = Index()

31
ios/feedless/Types.swift Normal file
View File

@ -0,0 +1,31 @@
//
// Types.swift
// feedless
//
// Created by Rogerio Chaves on 04/05/20.
// Copyright © 2020 Rogerio Chaves. All rights reserved.
//
struct Post: Codable {
public var text: String
}
struct AuthorContent<T: Codable>: Codable {
public var author: String
public var content: T
}
struct Entry<T: Codable>: Codable {
public var key: String
public var value: T
}
struct Profile: Codable {
public var id: String
public var name: String
public var image: String
}
struct User : Codable {
public var profile: Profile?
}

28
ios/feedless/Utils.swift Normal file
View File

@ -0,0 +1,28 @@
//
// Utils.swift
// feedless
//
// Created by Rogerio Chaves on 04/05/20.
// Copyright © 2020 Rogerio Chaves. All rights reserved.
//
import UIKit
import SwiftUI
extension String {
func image() -> UIImage? {
let nsString = (self as NSString)
let font = UIFont.systemFont(ofSize: 16) // you can change your font size here
let stringAttributes = [NSAttributedString.Key.font: font]
let imageSize = nsString.size(withAttributes: stringAttributes)
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0) // begin image context
UIColor.clear.set() // clear background
UIRectFill(CGRect(origin: CGPoint(), size: imageSize)) // set rect size
nsString.draw(at: CGPoint.zero, withAttributes: stringAttributes) // draw text within rect
let image = UIGraphicsGetImageFromCurrentImageContext() // create image from context
UIGraphicsEndImageContext() // end image context
return image ?? UIImage()
}
}

View File

@ -8,13 +8,60 @@
import SwiftUI
struct Index: View {
var body: some View {
NavigationView {
NavigationLink(destination: Login()) {
Text("Login")
class Context: ObservableObject {
@Published var responding:Bool = false
@Published var loggedIn:Bool = false
@Published var profile:Profile? = nil
func fetch() {
let url = URL(string: "http://127.0.0.1:3000/user")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
if let todoData = data {
DispatchQueue.main.async {
self.responding = true
self.loggedIn = true
}
do {
let decodedData = try JSONDecoder().decode(User.self, from: todoData)
DispatchQueue.main.async {
self.profile = decodedData.profile
self.loggedIn = true
}
} catch {
print("Error loading user")
}
}
}.resume()
}
}
struct Index: View {
@ObservedObject var context = Context()
@State var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Group {
if (context.responding) {
if (context.loggedIn) {
Wall()
} else {
NavigationView {
NavigationLink(destination: Login()) {
Text("Login")
}
.navigationBarTitle(Text("Index"))
}
}
} else {
Text("Waiting for server")
}
}
.onReceive(self.timer) { (_) in
if (!self.context.responding) {
self.context.fetch()
}
.navigationBarTitle(Text("Index"))
}
}
}

View File

@ -8,31 +8,15 @@
import SwiftUI
struct Post: Codable {
public var text: String
}
struct AuthorContent<T: Codable>: Codable {
public var author: String
public var content: T
}
struct Entry<T: Codable>: Codable {
public var key: String
public var value: T
}
class FetchPosts: ObservableObject {
// 1.
@Published var posts = [Entry<AuthorContent<Post>>]()
@Published var posts = [Entry<AuthorContent<Post>>]()
init() {
let url = URL(string: "http://127.0.0.1:3000/posts")!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let todoData = data {
// 3.
let decodedData = try JSONDecoder().decode([Entry<AuthorContent<Post>>].self, from: todoData)
DispatchQueue.main.async {
self.posts = decodedData
@ -55,7 +39,6 @@ struct Wall: View {
var body: some View {
TabView(selection: $selection){
VStack {
// 2.
List(fetch.posts, id: \.key) { post in
VStack(alignment: .leading) {
Text(post.value.content.text)
@ -64,7 +47,8 @@ struct Wall: View {
}
.tabItem {
VStack {
Text("🙂")
Image(uiImage: "🙂".image()!).renderingMode(.original)
Text("Profile")
}
}
.tag(0)
@ -72,11 +56,12 @@ struct Wall: View {
.font(.title)
.tabItem {
VStack {
Text("👨‍👧‍👦")
Image(uiImage: "👨‍👧‍👦".image()!).renderingMode(.original)
Text("Friends")
}
}
.tag(1)
}
}.accentColor(Color.purple)
}
}

View File

@ -1,5 +1,4 @@
const fs = require("fs");
const leftpad = require("left-pad"); // I don't believe I'm depending on this
const pull = require("pull-stream");
const split = require("split-buffer");
const metrics = require("./metrics");