Assignment 3: Convergent Design
Pitch
SocialInfo is a social media app for journalists and others who want to stay up-to-date on breaking news. Many people already obtain their news from social media, but that news is mixed with tons of misinformation due to issues like realistic parody accounts, Photoshop, or plainly incorrect statements that are taken as fact. My app’s features are designed to cut down on that misinformation through both community and AI-based “context” statements for posts, where users can upvote posts to indicate that they like the post, or upvote context statements to indicate their helpfulness, as well as follow users who they would like to see more posts from.
Concepts
concept User
Purpose
authenticates an individual user of the app
Principle
after a user registers with a username and password,
they can authenticate as that user by providing a matching username
and password
State
registered: set User
username, password: registered -> one String
Actions
register (n, p: String, out u: User)
Adds a User `u` with username `n` and password `p` to the
set `registered`
authenticate (n, p: String, out u: User)
If a User with username `n` and password `p` is in
`registered`, return the User
concept User
Purpose
authenticates an individual user of the app
Principle
after a user registers with a username and password,
they can authenticate as that user by providing a matching username
and password
State
registered: set User
username, password: registered -> one String
Actions
register (n, p: String, out u: User)
Adds a User `u` with username `n` and password `p` to the
set `registered`
authenticate (n, p: String, out u: User)
If a User with username `n` and password `p` is in
`registered`, return the User
concept Session [User]
Purpose
provides a way for users to remain authenticated for an
extended period of time
Principle
after a session starts (and before it ends), the getUser
action returns the user identified at the start
State
active: set Session
user: active -> one User
Actions
start (u: User, out s: Session)
Add a new Session to `active` for user `u`
getUser (s: Session, out u: User)
Given Session `s`, confirm that the `s` is in `active`
and return the user `u` it belongs to
end (s: Session)
Remove Session `s` from set `active`
concept Session [User]
Purpose
provides a way for users to remain authenticated for an
extended period of time
Principle
after a session starts (and before it ends), the getUser
action returns the user identified at the start
State
active: set Session
user: active -> one User
Actions
start (u: User, out s: Session)
Add a new Session to `active` for user `u`
getUser (s: Session, out u: User)
Given Session `s`, confirm that the `s` is in `active`
and return the user `u` it belongs to
end (s: Session)
Remove Session `s` from set `active`
concept Post [User]
Purpose
publish viewable content
Principle
A user can create a post, which can be viewed in
date-order with allPosts, or with a filter such that only
posts from certain users appear (using getPostsByUsers).
The posts can also be deleted.
State
posts: set Post
author: posted -> one User
body: posted -> one String
date: posted -> one Date
Actions
create(u: User, s: String, out p: Post)
Creates a new Post `p` with author `u` with body `s`
where `date` is the current date and adds it to the set
of `posts`
allPosts(out p: set Post)
Returns the set of all `posts` in order sorted on `date`
getPostsByUsers(u: set User, out p: set Post)
Returns the set of all `posts` by users in set `u` in
order sorted on `date`
delete(p: Post)
Removes post `p` from the set of posts
concept Post [User]
Purpose
publish viewable content
Principle
A user can create a post, which can be viewed in
date-order with allPosts, or with a filter such that only
posts from certain users appear (using getPostsByUsers).
The posts can also be deleted.
State
posts: set Post
author: posted -> one User
body: posted -> one String
date: posted -> one Date
Actions
create(u: User, s: String, out p: Post)
Creates a new Post `p` with author `u` with body `s`
where `date` is the current date and adds it to the set
of `posts`
allPosts(out p: set Post)
Returns the set of all `posts` in order sorted on `date`
getPostsByUsers(u: set User, out p: set Post)
Returns the set of all `posts` by users in set `u` in
order sorted on `date`
delete(p: Post)
Removes post `p` from the set of posts
concept Follow [User]
Purpose
allows a user to observe changes made by another user
Principle
a user can follow another user or unfollow them if already
followed, and getFollowing returns all users that a given
user follows
State
following: User -> set User
Actions
follow(u: User, v: User)
Add `v` to `following` set for user `u`
unfollow(u: User, v: User)
If `v` is in the set `following` for User `u`, remove `v`
from the `following` set for User `u`
getFollowing(u: User, out v: set User)
Return set of all users `v` that User `u` maps to
concept Follow [User]
Purpose
allows a user to observe changes made by another user
Principle
a user can follow another user or unfollow them if already
followed, and getFollowing returns all users that a given
user follows
State
following: User -> set User
Actions
follow(u: User, v: User)
Add `v` to `following` set for user `u`
unfollow(u: User, v: User)
If `v` is in the set `following` for User `u`, remove `v`
from the `following` set for User `u`
getFollowing(u: User, out v: set User)
Return set of all users `v` that User `u` maps to
concept Upvote [User, Item]
Purpose
indicates approval value of an item
Principle
a user can upvote an item, or unupvote to remove their vote
if they already cast one. The countVotes action returns the
number of votes that an item has.
State
voters: Item -> set User
Actions
upvote(u: User, i: Item)
Adds user `u` to the set of `voters` for item `i`
unupvote(u: User, i: Item)
If `u` is already in set `voters` for item i, remove user
`u` from the set `voters` for item `i`
countVotes(i: Item, out n: Integer)
Returns the size of set `voters` for a given item `i`
concept Upvote [User, Item]
Purpose
indicates approval value of an item
Principle
a user can upvote an item, or unupvote to remove their vote
if they already cast one. The countVotes action returns the
number of votes that an item has.
State
voters: Item -> set User
Actions
upvote(u: User, i: Item)
Adds user `u` to the set of `voters` for item `i`
unupvote(u: User, i: Item)
If `u` is already in set `voters` for item i, remove user
`u` from the set `voters` for item `i`
countVotes(i: Item, out n: Integer)
Returns the size of set `voters` for a given item `i`
concept Context [User, Item]
Purpose
provides additional info for an item
Principle
an autogenerated context can be added to an item, or a user
can add a context to an item. The getParentItem action returns
the item a context belongs to, while the getContextsForItem
returns all the contexts that an item has. A context can be shown
or hidden, as well as deleted.
State
contexts: set Context
items: Context -> one Item
author: Context -> one User
body: Context -> one String
visible: Context -> one Boolean
Actions
autogenerate(i: Item, out c: Context)
Create new Context `c` with an automatically generated body
for item `i` and add it to `contexts`
add(u: User, s: String, i: Item, out c: Context)
Create new Context `c` with author `u` and body `s` for item
`i` and add it to `contexts`
getParentItem(c: Context, out i: Item)
Returns the item `i` that `c` maps to
getContextsForItem(i: Item, out c: set Context)
Returns the set of all contexts that map to item `i`
show(c: Context)
Set `visible` for context `c` to True
hide(c: Context)
Set `visible` for context `c` to False
delete(c: Context)
Remove context `c` from set of `contexts`
concept Context [User, Item]
Purpose
provides additional info for an item
Principle
an autogenerated context can be added to an item, or a user
can add a context to an item. The getParentItem action returns
the item a context belongs to, while the getContextsForItem
returns all the contexts that an item has. A context can be shown
or hidden, as well as deleted.
State
contexts: set Context
items: Context -> one Item
author: Context -> one User
body: Context -> one String
visible: Context -> one Boolean
Actions
autogenerate(i: Item, out c: Context)
Create new Context `c` with an automatically generated body
for item `i` and add it to `contexts`
add(u: User, s: String, i: Item, out c: Context)
Create new Context `c` with author `u` and body `s` for item
`i` and add it to `contexts`
getParentItem(c: Context, out i: Item)
Returns the item `i` that `c` maps to
getContextsForItem(i: Item, out c: set Context)
Returns the set of all contexts that map to item `i`
show(c: Context)
Set `visible` for context `c` to True
hide(c: Context)
Set `visible` for context `c` to False
delete(c: Context)
Remove context `c` from set of `contexts`
app SocialInfo
includes
User
Session [User.User]
Post [User.User]
Context [User.User, Post.Post]
Upvote [User.User, Post.Post]
Upvote [User.User, Context.Context]
Follow [User.User]
sync register (username, password: String, out user: User)
u = User.register (username, password)
sync login (username, password: String, out user: User, out s: Session)
when User.authenticate (username, password, user)
s = Session.start (user)
sync logout (s: Session)
Session.end (s)
sync authenticate (s: Session, out u: User)
u = Session.getUser (s)
sync makePost(session: Session, str: String)
u = Session.getUser(session)
p = Post.create(u, str)
c = Context.autogenerate(p)
Context.show(c)
sync submitContext(session: Session, str: String, p: Post, out c: Context)
u = Session.getUser(session)
if p.author != u
c = Context.add(u, str, p)
sync deleteContext(s: Session, c: Context)
u = Session.getUser(s)
if u == c.author
Context.delete(c)
sync upvoteContext(s: Session, c: Context)
u = Session.getUser(s)
Upvote.upvote(u, c)
p = Context.getParentItem(c)
ctxs = Context.getContextsForItem(p)
top = i that has the max Upvote.countVotes(i) for i in ctxs
rest = ctxs \ top
Context.show(top)
Context.hide(r) for r in rest
sync unupvoteContext(s: Session, c: Context)
u = Session.getUser(s)
Upvote.unupvote(u, c)
p = Context.getParentItem(c)
ctxs = Context.getContextsForItem(p)
top = i that has the max Upvote.countVotes(i) for i in ctxs
rest = ctxs \ top
Context.show(top)
Context.hide(r) for r in rest
sync upvotePost(s: Session, p: Post)
u = Session.getUser(s)
Upvote.upvote(u, p)
sync unupvotePost(s: Session, p: Post)
u = Session.getUser(s)
Upvote.unupvote(u, p)
sync deletePost(s: Session, p: Post)
u = Session.getUser(s)
if u == p.author
contexts = Context.getContextsForItem(p)
Context.delete(c) for c in contexts
Post.delete(p)
sync getGlobalFeed(out feed: set Post)
feed = Post.allPosts()
sync getUserFeed(s: Session, out feed: set Post)
u = Session.getUser(s)
f = Follower.getFollowing(u)
feed = Post.getPostsByUsers(f)
sync getPostsByUser(u: User, out posts: set Post)
posts = Post.getPostsByUsers({u})
app SocialInfo
includes
User
Session [User.User]
Post [User.User]
Context [User.User, Post.Post]
Upvote [User.User, Post.Post]
Upvote [User.User, Context.Context]
Follow [User.User]
sync register (username, password: String, out user: User)
u = User.register (username, password)
sync login (username, password: String, out user: User, out s: Session)
when User.authenticate (username, password, user)
s = Session.start (user)
sync logout (s: Session)
Session.end (s)
sync authenticate (s: Session, out u: User)
u = Session.getUser (s)
sync makePost(session: Session, str: String)
u = Session.getUser(session)
p = Post.create(u, str)
c = Context.autogenerate(p)
Context.show(c)
sync submitContext(session: Session, str: String, p: Post, out c: Context)
u = Session.getUser(session)
if p.author != u
c = Context.add(u, str, p)
sync deleteContext(s: Session, c: Context)
u = Session.getUser(s)
if u == c.author
Context.delete(c)
sync upvoteContext(s: Session, c: Context)
u = Session.getUser(s)
Upvote.upvote(u, c)
p = Context.getParentItem(c)
ctxs = Context.getContextsForItem(p)
top = i that has the max Upvote.countVotes(i) for i in ctxs
rest = ctxs \ top
Context.show(top)
Context.hide(r) for r in rest
sync unupvoteContext(s: Session, c: Context)
u = Session.getUser(s)
Upvote.unupvote(u, c)
p = Context.getParentItem(c)
ctxs = Context.getContextsForItem(p)
top = i that has the max Upvote.countVotes(i) for i in ctxs
rest = ctxs \ top
Context.show(top)
Context.hide(r) for r in rest
sync upvotePost(s: Session, p: Post)
u = Session.getUser(s)
Upvote.upvote(u, p)
sync unupvotePost(s: Session, p: Post)
u = Session.getUser(s)
Upvote.unupvote(u, p)
sync deletePost(s: Session, p: Post)
u = Session.getUser(s)
if u == p.author
contexts = Context.getContextsForItem(p)
Context.delete(c) for c in contexts
Post.delete(p)
sync getGlobalFeed(out feed: set Post)
feed = Post.allPosts()
sync getUserFeed(s: Session, out feed: set Post)
u = Session.getUser(s)
f = Follower.getFollowing(u)
feed = Post.getPostsByUsers(f)
sync getPostsByUser(u: User, out posts: set Post)
posts = Post.getPostsByUsers({u})
Dependency Diagram
Wireframing
Design Tradeoffs
- Text-only (Add image and video attachments, or not): No media like videos, images, etc… All that must be posted as external links (which are included in String), rather than attached directly as files. I wanted to reduce the number of concepts I used in order to leave some room for my novel ones.
- No Profile (Allow user profile customization or not): No profile picture, description, pronouns, display name, etc, only username. I was a bit torn on this one, since it can make users harder to distinguish at a glance, but it’s one less concept that I’d have to add, and since I don’t have verification, then hopefully people will be less likely to fall for impersonations if there are fewer aspects available to imitate, and usernames must be unique.
- Dislike Button? (Upvote only or upvote + downvote): I decided not to have a dislike button, only a like button. On the one hand, disliking could help show other users what is unhelpful. On the other, sometimes dislikes can be abused by other users. I decided to only add upvotes, not downvotes, to avoid the possibility of abuse, especially in the case of brigading where users gang up to intentionally dislike a post for an arbitrary reason.
- Default Feed (Default to following vs global feed): I decided to default to following because I think most users would prefer that. Even journalists have certain topics they care about more and would probably like to curate their experience somewhat, so having Following be the default feed makes it more convenient. This could promote echo chambers caused by following only people you agree with, but I don't think the global feed is nearly useful enough to be promoted as the default.