Compare commits
8 Commits
2972bad192
...
75e7511c7c
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 75e7511c7c | |
Shadowfacts | eba9e4be96 | |
Shadowfacts | 77bca46197 | |
Shadowfacts | 7d071d971c | |
Shadowfacts | 6c0fc06c21 | |
Shadowfacts | f65c5752bc | |
Shadowfacts | 610af799ec | |
Shadowfacts | 0ca9d02499 |
|
@ -1,3 +1,5 @@
|
|||
.DS_Store
|
||||
|
||||
# The directory Mix will write compiled artifacts to.
|
||||
/_build/
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
/* This file is for your main application css. */
|
||||
|
||||
@import "./phoenix.css";
|
|
@ -0,0 +1,5 @@
|
|||
/* This file is for your main application css. */
|
||||
|
||||
/* @import "./phoenix.css"; */
|
||||
@import "./normalize.css";
|
||||
@import "./clacks.scss";
|
|
@ -0,0 +1,139 @@
|
|||
$sans-serif: -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif;
|
||||
$serif: "Times New Roman", serif;
|
||||
$tint-color: #4b43e0;
|
||||
// $link-color: blue;
|
||||
$link-color: $tint-color;
|
||||
$hr-color: darkgray;
|
||||
|
||||
body {
|
||||
font-family: $sans-serif;
|
||||
// always show scrollbar so effective page width doesn't change
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
a,
|
||||
button.btn-link {
|
||||
background: none;
|
||||
border: none;
|
||||
color: $link-color;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
button[type=submit]:not(.btn-link) {
|
||||
font-family: $sans-serif;
|
||||
padding: 0.25rem 2rem;
|
||||
background-color: $tint-color;
|
||||
border: 1px solid darken($tint-color, 20%);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: darken($tint-color, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid $hr-color;
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus {
|
||||
outline: none;
|
||||
border: 2px solid $tint-color;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
header {
|
||||
nav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
display: inline;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul.status-list {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f2f2f2;
|
||||
|
||||
.status-meta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
|
||||
.status-author-nickname,
|
||||
.status-author-username,
|
||||
.status-meta-right {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
margin-right: 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.status-author-username {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.status-meta-right {
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.status-content {
|
||||
font-family: $serif;
|
||||
font-size: 1.2rem;
|
||||
margin: 1rem 0;
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.compose-status {
|
||||
textarea {
|
||||
display: block;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 4rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
|
||||
// we want 100% width to include the border
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,350 @@
|
|||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input { /* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select { /* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10.
|
||||
* 2. Remove the padding in IE 10.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10+.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// We need to import the CSS so that webpack will load it.
|
||||
// The MiniCssExtractPlugin is used to separate it out into
|
||||
// its own CSS file.
|
||||
import css from "../css/app.css"
|
||||
import css from "../css/app.scss"
|
||||
|
||||
// webpack automatically bundles all modules in your
|
||||
// entry points. Those entry points can be configured
|
||||
|
|
|
@ -2779,6 +2779,17 @@
|
|||
"integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=",
|
||||
"dev": true
|
||||
},
|
||||
"clone-deep": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
|
||||
"integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-plain-object": "^2.0.4",
|
||||
"kind-of": "^6.0.2",
|
||||
"shallow-clone": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"clone-response": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
|
||||
|
@ -9332,6 +9343,36 @@
|
|||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.26.3",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.26.3.tgz",
|
||||
"integrity": "sha512-5NMHI1+YFYw4sN3yfKjpLuV9B5l7MqQ6FlkTcC4FT+oHbBRUZoSjHrrt/mE0nFXJyY2kQtU9ou9HxvFVjLFuuw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=2.0.0 <4.0.0"
|
||||
}
|
||||
},
|
||||
"sass-loader": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.3.1.tgz",
|
||||
"integrity": "sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"clone-deep": "^4.0.1",
|
||||
"loader-utils": "^1.0.1",
|
||||
"neo-async": "^2.5.0",
|
||||
"pify": "^4.0.1",
|
||||
"semver": "^6.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
|
@ -9412,6 +9453,15 @@
|
|||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"shallow-clone": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
|
||||
"integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"kind-of": "^6.0.2"
|
||||
}
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
"optimize-css-assets-webpack-plugin": "^4.0.0",
|
||||
"uglifyjs-webpack-plugin": "^1.2.4",
|
||||
"webpack": "4.4.0",
|
||||
"webpack-cli": "^2.0.10"
|
||||
"webpack-cli": "^2.0.10",
|
||||
"sass": "^1.26.3",
|
||||
"sass-loader": "^7.3.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,8 +29,8 @@ module.exports = (env, options) => ({
|
|||
}
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader']
|
||||
test: /\.scss$/,
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -32,6 +32,7 @@ defmodule Clacks.ActivityPub do
|
|||
actor :: String.t(),
|
||||
html :: String.t(),
|
||||
context :: String.t() | nil,
|
||||
in_reply_to :: String.t() | nil,
|
||||
id :: String.t() | nil,
|
||||
published :: DateTime.t(),
|
||||
to :: [String.t()],
|
||||
|
@ -41,6 +42,7 @@ defmodule Clacks.ActivityPub do
|
|||
actor,
|
||||
html,
|
||||
context \\ nil,
|
||||
in_reply_to \\ nil,
|
||||
id \\ nil,
|
||||
published \\ DateTime.utc_now(),
|
||||
to \\ [@public],
|
||||
|
@ -61,6 +63,7 @@ defmodule Clacks.ActivityPub do
|
|||
"content" => html,
|
||||
"conversation" => context,
|
||||
"context" => context,
|
||||
"inReplyTo" => in_reply_to,
|
||||
"published" => published |> DateTime.to_iso8601()
|
||||
}
|
||||
end
|
||||
|
@ -135,6 +138,6 @@ defmodule Clacks.ActivityPub do
|
|||
|
||||
@spec context_id(id :: String.t()) :: String.t()
|
||||
def context_id(id) do
|
||||
"data:,clickityclack" <> id
|
||||
"data:,clickityclack/" <> id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,7 +25,7 @@ defmodule Clacks.ActivityPub.Federator do
|
|||
|
||||
@spec federate(activity :: map(), inbox :: String.t()) :: :ok | {:error, any()}
|
||||
def federate(%{"actor" => actor_id} = activity, inbox) do
|
||||
Logger.info("Federating #{activity["id"]} to #{inbox}")
|
||||
Logger.debug("Federating #{activity["id"]} to #{inbox}")
|
||||
%{host: inbox_host, path: inbox_path} = URI.parse(inbox)
|
||||
|
||||
{:ok, body} = Jason.encode(activity)
|
||||
|
@ -71,7 +71,7 @@ defmodule Clacks.ActivityPub.Federator do
|
|||
shared_inbox_for(actor)
|
||||
|
||||
true ->
|
||||
actor.data["inbox"]
|
||||
actor["inbox"]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -6,20 +6,29 @@ defmodule Clacks.Timeline do
|
|||
@public "https://www.w3.org/ns/activitystreams#Public"
|
||||
@timeline_types ["Create", "Announce"]
|
||||
|
||||
@spec actor_timeline(actor :: Actor.t(), only_public :: boolean(), params :: map()) :: [
|
||||
@spec actor_timeline(
|
||||
actor :: Actor.t(),
|
||||
params :: map(),
|
||||
only_public :: boolean(),
|
||||
actors :: boolean()
|
||||
) :: [
|
||||
Activity.t()
|
||||
]
|
||||
def actor_timeline(actor, only_public \\ true, params) do
|
||||
def actor_timeline(actor, params, only_public \\ true, actors \\ false) do
|
||||
Activity
|
||||
|> restrict_to_actor(actor.ap_id)
|
||||
|> restrict_to_types(@timeline_types)
|
||||
|> restrict_to_public(only_public)
|
||||
|> paginate(params)
|
||||
|> limit(^Map.get(params, "limit", 20))
|
||||
|> join_with_actors(actors)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec home_timeline(user :: User.t(), params :: map()) :: [Activity.t()]
|
||||
def home_timeline(user, params) do
|
||||
@spec home_timeline(user :: User.t(), params :: map(), actors :: boolean()) :: [
|
||||
Activity.t()
|
||||
]
|
||||
def home_timeline(user, params, actors \\ false) do
|
||||
user =
|
||||
case user.actor do
|
||||
%Ecto.Association.NotLoaded{} ->
|
||||
|
@ -35,8 +44,10 @@ defmodule Clacks.Timeline do
|
|||
fragment("?->>'actor'", a.data) == ^user.actor.ap_id or
|
||||
fragment("?->>'actor'", a.data) in ^user.actor.followers
|
||||
)
|
||||
|> restrict_to_types([@timeline_types])
|
||||
|> restrict_to_types(@timeline_types)
|
||||
|> paginate(params)
|
||||
|> limit(^Map.get(params, "limit", 20))
|
||||
|> join_with_actors(actors)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
|
@ -57,4 +68,12 @@ defmodule Clacks.Timeline do
|
|||
end
|
||||
|
||||
defp restrict_to_public(query, false), do: query
|
||||
|
||||
defp join_with_actors(query, true) do
|
||||
query
|
||||
|> join(:left, [o], a in Actor, on: a.ap_id == fragment("?->>'actor'", o.data))
|
||||
|> select([o, a], {o, a})
|
||||
end
|
||||
|
||||
defp join_with_actors(query, false), do: query
|
||||
end
|
||||
|
|
|
@ -18,7 +18,27 @@ defmodule ClacksWeb.ActivitiesController do
|
|||
end
|
||||
|
||||
_ ->
|
||||
resp(conn, 404, "Not Found")
|
||||
case conn.assigns[:format] do
|
||||
"activity+json" ->
|
||||
conn
|
||||
|> put_status(404)
|
||||
|> json(%{error: "Not Found"})
|
||||
|
||||
"html" ->
|
||||
resp(conn, 404, "Not Found")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_status(conn, %{"id" => status_id}) do
|
||||
case Activity.get(status_id) do
|
||||
%Activity{local: true, data: data} ->
|
||||
json(conn, data)
|
||||
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(404)
|
||||
|> json(%{error: "Not Found"})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,13 +4,15 @@ defmodule ClacksWeb.FrontendController do
|
|||
alias ClacksWeb.Router.Helpers, as: Routes
|
||||
alias ClacksWeb.Endpoint
|
||||
|
||||
@public "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
def index(%Plug.Conn{assigns: %{user: user}} = conn, params) do
|
||||
user = Repo.preload(user, :actor)
|
||||
|
||||
render(conn, "home.html", %{
|
||||
user: user,
|
||||
actor: user.actor,
|
||||
statuses: Timeline.home_timeline(user, params)
|
||||
statuses_with_authors: Timeline.home_timeline(user, params, true)
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -40,7 +42,7 @@ defmodule ClacksWeb.FrontendController do
|
|||
end
|
||||
|
||||
defp actor_statuses(actor, params, only_public: only_public) do
|
||||
Timeline.actor_timeline(actor, only_public, params)
|
||||
Timeline.actor_timeline(actor, params, only_public)
|
||||
end
|
||||
|
||||
def status(conn, %{"id" => id}) do
|
||||
|
@ -48,23 +50,67 @@ defmodule ClacksWeb.FrontendController do
|
|||
|
||||
with %Activity{
|
||||
local: true,
|
||||
data:
|
||||
%{
|
||||
"type" => "Create",
|
||||
"object" => %{"type" => "Note", "attributedTo" => author_id} = note
|
||||
} = data
|
||||
} = activity <- Activity.get(id),
|
||||
%Actor{} = author <- Actor.get_by_ap_id(author_id) do
|
||||
case conn.assigns[:format] do
|
||||
"activity+json" ->
|
||||
json(conn, data)
|
||||
|
||||
"html" ->
|
||||
render(conn, "status.html", %{
|
||||
current_user: current_user,
|
||||
status: activity,
|
||||
author: author
|
||||
})
|
||||
end
|
||||
else
|
||||
nil ->
|
||||
case conn.assigns[:format] do
|
||||
"activity+json" ->
|
||||
conn
|
||||
|> put_status(404)
|
||||
|> json(%{error: "Not Found"})
|
||||
|
||||
"html" ->
|
||||
resp(conn, 404, "Not Found")
|
||||
end
|
||||
|
||||
%Activity{local: false, data: %{"id" => ap_id}} ->
|
||||
case conn.assigns[:format] do
|
||||
"activity+json" ->
|
||||
conn
|
||||
|> put_status(404)
|
||||
|> json(%{error: "Not Found"})
|
||||
|
||||
"html" ->
|
||||
redirect(conn, external: ap_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def reply(conn, %{"id" => id}) do
|
||||
current_user = conn.assigns[:user]
|
||||
|
||||
with %Activity{
|
||||
data: %{
|
||||
"type" => "Create",
|
||||
"object" => %{"type" => "Note", "attributedTo" => author_id} = note
|
||||
"object" => %{"type" => "Note", "attributedTo" => author_id}
|
||||
}
|
||||
} <- Activity.get(id),
|
||||
} = activity <- Activity.get(id),
|
||||
%Actor{} = author <- Actor.get_by_ap_id(author_id) do
|
||||
render(conn, "status.html", %{
|
||||
current_user: current_user,
|
||||
note: note,
|
||||
status: activity,
|
||||
author: author
|
||||
})
|
||||
else
|
||||
nil ->
|
||||
put_status(conn, 404)
|
||||
|
||||
%Activity{local: false, data: %{"id" => ap_id}} ->
|
||||
redirect(conn, external: ap_id)
|
||||
_ ->
|
||||
resp(conn, 404, "Not Found")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -89,7 +135,7 @@ defmodule ClacksWeb.FrontendController do
|
|||
def post_status(conn, %{"content" => content} = params) do
|
||||
current_user = conn.assigns[:user] |> Repo.preload(:actor)
|
||||
|
||||
note = ActivityPub.note(current_user.actor.ap_id, content)
|
||||
note = note_for_posting(current_user, params)
|
||||
note_changeset = Object.changeset_for_creating(note)
|
||||
{:ok, object} = Repo.insert(note_changeset)
|
||||
|
||||
|
@ -104,4 +150,31 @@ defmodule ClacksWeb.FrontendController do
|
|||
path = Map.get(params, "continue", Routes.frontend_path(Endpoint, :status, activity.id))
|
||||
redirect(conn, to: path)
|
||||
end
|
||||
|
||||
defp note_for_posting(current_user, %{"content" => content, "in_reply_to" => in_reply_to_ap_id}) do
|
||||
with %Activity{data: %{"context" => context, "actor" => in_reply_to_actor}} <-
|
||||
Activity.get_by_ap_id(in_reply_to_ap_id) do
|
||||
to = [in_reply_to_actor, @public]
|
||||
# todo: followers
|
||||
cc = []
|
||||
|
||||
ActivityPub.note(
|
||||
current_user.actor.ap_id,
|
||||
content,
|
||||
context,
|
||||
in_reply_to_ap_id,
|
||||
nil,
|
||||
DateTime.utc_now(),
|
||||
to,
|
||||
cc
|
||||
)
|
||||
else
|
||||
_ ->
|
||||
ActivityPub.note(current_user.actor.ap_id, content)
|
||||
end
|
||||
end
|
||||
|
||||
defp note_for_posting(current_user, %{"content" => content}) do
|
||||
ActivityPub.note(current_user.actor.ap_id, content)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,32 +1,28 @@
|
|||
defmodule ClacksWeb.OutboxController do
|
||||
use ClacksWeb, :controller
|
||||
alias Clacks.{Repo, Actor, Activity}
|
||||
alias Clacks.{Repo, Actor, Activity, Timeline}
|
||||
import Ecto.Query
|
||||
|
||||
@context "https://www.w3.org/ns/activitystreams"
|
||||
@outbox_types ["Create", "Announce"]
|
||||
|
||||
plug :get_actor
|
||||
|
||||
defp get_actor(%Plug.Conn{path_params: %{"nickname" => nickname}} = conn, _opts) do
|
||||
case Actor.get_by_nickname(nickname) do
|
||||
nil ->
|
||||
conn
|
||||
|> put_status(404)
|
||||
|
||||
%Actor{local: false} ->
|
||||
conn
|
||||
|> put_status(404)
|
||||
|
||||
actor ->
|
||||
defp get_actor(%Plug.Conn{path_params: %{"username" => username}} = conn, _opts) do
|
||||
case Actor.get_by_nickname(username) do
|
||||
%Actor{local: true} = actor ->
|
||||
assign(conn, :actor, actor)
|
||||
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(404)
|
||||
|> json(%{error: "Not Found"})
|
||||
end
|
||||
end
|
||||
|
||||
def outbox(conn, params) when params == %{} do
|
||||
actor = conn.assigns[:actor]
|
||||
|
||||
activities = Repo.all(outbox_query(params, actor))
|
||||
activities = Timeline.actor_timeline(actor, true, params)
|
||||
|
||||
data = %{
|
||||
"@context" => @context,
|
||||
|
@ -43,7 +39,7 @@ defmodule ClacksWeb.OutboxController do
|
|||
def outbox(conn, params) do
|
||||
actor = conn.assigns[:actor]
|
||||
|
||||
activities = Repo.all(outbox_query(params, actor))
|
||||
activities = Timeline.actor_timeline(actor, true, params)
|
||||
|
||||
data =
|
||||
outbox_page(conn, params, activities)
|
||||
|
@ -54,14 +50,6 @@ defmodule ClacksWeb.OutboxController do
|
|||
|> json(data)
|
||||
end
|
||||
|
||||
defp outbox_query(params, %Actor{ap_id: ap_id}) do
|
||||
Activity
|
||||
|> where([a], a.actor == ^ap_id)
|
||||
|> where([a], fragment("?->>'type'", a.data) in @outbox_types)
|
||||
|> Clacks.Paginator.paginate(params)
|
||||
|> limit(^Map.get(params, "limit", 20))
|
||||
end
|
||||
|
||||
defp outbox_page(conn, pagination_params, activities) do
|
||||
last_id = List.last(activities).id
|
||||
|
||||
|
|
|
@ -4,7 +4,20 @@ defmodule ClacksWeb.Plug.Format do
|
|||
def init(opts), do: opts
|
||||
|
||||
def call(conn, _opts) do
|
||||
format = Phoenix.Controller.get_format(conn)
|
||||
conn = fetch_query_params(conn)
|
||||
|
||||
format =
|
||||
case conn.query_params do
|
||||
%{"format" => format} when format in ["activity+json", "html"] ->
|
||||
format
|
||||
|
||||
%{"format" => "json"} ->
|
||||
"activity+json"
|
||||
|
||||
_ ->
|
||||
Phoenix.Controller.get_format(conn)
|
||||
end
|
||||
|
||||
assign(conn, :format, format)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -51,13 +51,15 @@ defmodule ClacksWeb.Router do
|
|||
pipe_through :browser_maybe_authenticated
|
||||
|
||||
get "/", FrontendController, :index
|
||||
get "/status/:id", FrontendController, :status
|
||||
post "/post", FrontendController, :post_status
|
||||
end
|
||||
|
||||
scope "/", ClacksWeb do
|
||||
pipe_through :browser
|
||||
pipe_through :browser_authenticated
|
||||
|
||||
post "/post", FrontendController, :post_status
|
||||
|
||||
get "/status/:id/reply", FrontendController, :reply
|
||||
end
|
||||
|
||||
scope "/", ClacksWeb do
|
||||
|
@ -80,6 +82,7 @@ defmodule ClacksWeb.Router do
|
|||
get "/users/:username", ActorController, :get
|
||||
get "/activities/:id", ActivitiesController, :get
|
||||
get "/objects/:id", ObjectsController, :get
|
||||
get "/status/:id", FrontendController, :status
|
||||
end
|
||||
|
||||
# Other scopes may use custom stacks.
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<div class="status">
|
||||
<div class="status-meta">
|
||||
<h2 class="status-author-nickname">
|
||||
<a href="<%= @author.ap_id %>">
|
||||
<%= @author.data["preferredUsername"] %>
|
||||
</a>
|
||||
</h2>
|
||||
<h3 class="status-author-username">
|
||||
<a href="<%= @author.ap_id %>">
|
||||
<%= display_username(@author) %>
|
||||
</a>
|
||||
</h3>
|
||||
<p class="status-meta-right">
|
||||
<span><%= display_timestamp(@note["published"]) %></span>
|
||||
<a href="<%= @note["url"] %>" class="status-permalink">Permalink</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="status-content">
|
||||
<%= @note["content"] %>
|
||||
</div>
|
||||
<div class="status-actions">
|
||||
<a href="<%= Routes.frontend_path(@conn, :reply, @status.id) %>">Reply</a>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,7 @@
|
|||
<ul class="status-list">
|
||||
<%= for {status, author} <- @statuses_with_authors do %>
|
||||
<li>
|
||||
<%= render "_status.html", conn: @conn, author: author, status: status, note: status.data["object"] %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
|
@ -1,19 +1,9 @@
|
|||
<h1>Home</h1>
|
||||
<p>Logged in as <%= @user.username %></p>
|
||||
|
||||
<%= form_tag Routes.frontend_path(@conn, :post_status), method: :post do %>
|
||||
<textarea id="content" name="content" cols="30" rows="10"></textarea>
|
||||
<%= form_tag Routes.frontend_path(@conn, :post_status), method: :post, class: "compose-status" do %>
|
||||
<textarea id="content" name="content" rows="5" placeholder="What's up?" required></textarea>
|
||||
<%= submit "Post" %>
|
||||
<hr>
|
||||
<% end %>
|
||||
|
||||
<ul>
|
||||
<%= for status <- @statuses do %>
|
||||
<li>
|
||||
<div class="status">
|
||||
<div class="status-content">
|
||||
<%= status.data["object"]["content"] %>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<%= render "_timeline.html", conn: @conn, statuses_with_authors: @statuses_with_authors %>
|
||||
|
|
|
@ -2,14 +2,4 @@
|
|||
<h2>@<%= @actor.data["name"] %></h2>
|
||||
<p><%= @actor.data["summary"] %></p>
|
||||
|
||||
<ul>
|
||||
<%= for status <- @statuses do %>
|
||||
<li>
|
||||
<div class="status">
|
||||
<div class="status-content">
|
||||
<%= status.data["object"]["content"] %>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<%= render "_timeline.html", conn: @conn, statuses_with_authors: Enum.map(@statuses, &({&1, @actor})) %>
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
<div class="status">
|
||||
<h2>
|
||||
<a href="<%= @author.ap_id %>">
|
||||
<%= @author.data["preferredUsername"] %>
|
||||
</a>
|
||||
</h2>
|
||||
<h3>
|
||||
<a href="<%= @author.ap_id %>">
|
||||
<%= @author.data["name"] %>
|
||||
</a>
|
||||
</h3>
|
||||
<div class="status-content">
|
||||
<%= @note["content"] %>
|
||||
</div>
|
||||
</div>
|
||||
<%= render "_status.html", conn: @conn, author: @author, status: @status, note: @status.data["object"] %>
|
||||
|
||||
<hr>
|
||||
|
||||
<%= form_tag Routes.frontend_path(@conn, :post_status), method: :post, class: "compose-status" do %>
|
||||
<input type="hidden" name="in_reply_to" value="<%= @status.data["object"]["id"] %>">
|
||||
<textarea id="content" name="content" rows="5" placeholder="Reply" required></textarea>
|
||||
<%= submit "Post" %>
|
||||
<hr>
|
||||
<% end %>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title><%= assigns[:page_title] || "Clacks" %></title>
|
||||
<title><%= assigns[:page_title] || instance_name() %></title>
|
||||
<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -15,15 +15,20 @@
|
|||
<li><a href="/"><%= instance_name() %></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<%= if @conn.assigns[:user] do %>
|
||||
<%= if @conn.assigns[:user] do %>
|
||||
<li>
|
||||
Logged in as <a href="<%= Routes.actor_path(@conn, :get, @conn.assigns[:user].username) %>"><%= @conn.assigns[:user].username %></a>.
|
||||
</li>
|
||||
<li>
|
||||
<%= form_for @conn, Routes.login_path(@conn, :logout), [method: :post], fn f -> %>
|
||||
<%= submit "Log Out", class: "btn-link" %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
</li>
|
||||
<% else %>
|
||||
<li>
|
||||
<a href="<%= login_path(@conn) %>">Log In</a>
|
||||
<% end %>
|
||||
</li>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</nav>
|
||||
</section>
|
||||
|
|
|
@ -1,3 +1,46 @@
|
|||
defmodule ClacksWeb.FrontendView do
|
||||
use ClacksWeb, :view
|
||||
alias Clacks.Actor
|
||||
|
||||
@spec display_username(actor :: Actor.t()) :: String.t()
|
||||
|
||||
def display_username(%Actor{local: true, data: %{"name" => name}}) do
|
||||
"@" <> name
|
||||
end
|
||||
|
||||
def display_username(%Actor{local: false, ap_id: ap_id, data: %{"name" => name}}) do
|
||||
%URI{host: host} = URI.parse(ap_id)
|
||||
"@" <> name <> "@" <> host
|
||||
end
|
||||
|
||||
@absolute_timestamp_threshold 24 * 60 * 60
|
||||
|
||||
def display_timestamp(str) when is_binary(str) do
|
||||
display_timestamp(Timex.parse!(str, "{ISO:Extended}"))
|
||||
end
|
||||
|
||||
def display_timestamp(datetime) do
|
||||
diff = Timex.diff(Timex.now(), datetime, :seconds)
|
||||
|
||||
cond do
|
||||
diff < 60 ->
|
||||
# less than a minute, seconds
|
||||
"#{diff}sec"
|
||||
|
||||
diff < 60 * 60 ->
|
||||
# less than an hour, minutes
|
||||
"#{Integer.floor_div(diff, 60)}min"
|
||||
|
||||
diff < 60 * 60 * 24 ->
|
||||
# less than a day, hours
|
||||
"#{Integer.floor_div(diff, 60 * 60)}hr"
|
||||
|
||||
diff < 60 * 60 * 24 * 7 ->
|
||||
# less than a week, days
|
||||
"#{Integer.floor_div(diff, 60 * 60 * 24)}d"
|
||||
|
||||
true ->
|
||||
Timex.format!(datetime, "%FT%T%:z", :strftime)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue