diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3d7be86
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+mlmym
+VERSION
+*.toml
+*.txt
diff --git a/Dockerfile b/Dockerfile
index 1bd6c58..9b9ab73 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,6 +4,7 @@ WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . ./
+RUN git describe --tag > VERSION
RUN go build -v -o mlmym
FROM debian:bullseye-slim
@@ -14,4 +15,5 @@ RUN set -x && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -
COPY --from=builder /app/mlmym /app/mlmym
COPY --from=builder /app/templates /app/templates
COPY --from=builder /app/public /app/public
+COPY --from=builder /app/VERSION /app/VERSION
CMD ["./mlmym", "--addr", "0.0.0.0:8080"]
diff --git a/Makefile b/Makefile
index ca918f2..1b88d82 100644
--- a/Makefile
+++ b/Makefile
@@ -1,17 +1,18 @@
-.PHONY: dev reload serve style
+.PHONY: dev reload serve VERSION
-all:
- $(MAKE) -j3 --no-print-directory dev
+all: mlmym
-dev: reload serve style
+mlmym: VERSION
+ go build -v -o mlmym
+
+dev:
+ $(MAKE) -j2 --no-print-directory reload serve
reload:
- #websocketd --port=8080 watchexec -w public echo reload &>/dev/null
- websocketd --loglevel=fatal --port=8009 watchexec --no-vcs-ignore -e html,css,js -d 500 -w public 'echo "$$WATCHEXEC_WRITTEN_PATH"'
+ websocketd --loglevel=fatal --port=8009 watchexec --no-vcs-ignore -e html,css,js 'echo "$$WATCHEXEC_WRITTEN_PATH"'
-serve:
- #python -m http.server --directory ./public 8081 &>/dev/null
- watchexec -e go -r "go run . --addr 0.0.0.0:8008 -w"
+VERSION:
+ git describe --tag > $@
-style:
- npm run watchcss > /dev/null 2>&1
+serve: VERSION
+ DEBUG=true watchexec --no-vcs-ignore -e go -r "go run . --addr 0.0.0.0:8008 -w"
diff --git a/README.md b/README.md
index 4c17ef0..c2bde67 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,25 @@
# mlmym
a familiar desktop experience for [lemmy](https://join-lemmy.org).
-
+
-### deployment
+## deployment
```bash
docker run -it -p "8080:8080" ghcr.io/rystaf/mlmym:latest
```
-### config
+## config
Set the environment variable `LEMMY_DOMAIN` to run in single instance mode
```bash
docker run -it -e LEMMY_DOMAIN='lemmydomain.com' -p "8080:8080" ghcr.io/rystaf/mlmym:latest
```
+#### default user settings
+| environment variable | default |
+| -------------------- | ------- |
+| DARK | false |
+| HIDE_THUMBNAILS | false |
+| LISTING | All |
+| SORT | Hot |
+| COMMENT_SORT | Hot |
+
diff --git a/go.mod b/go.mod
index 88f3104..062f0de 100644
--- a/go.mod
+++ b/go.mod
@@ -3,15 +3,16 @@ module mlmym
go 1.19
require (
- github.com/cenkalti/backoff/v4 v4.2.0 // indirect
- github.com/dustin/go-humanize v1.0.1 // indirect
- github.com/google/go-querystring v1.1.0 // indirect
- github.com/gorilla/securecookie v1.1.1 // indirect
- github.com/gorilla/sessions v1.2.1 // indirect
- github.com/gorilla/websocket v1.4.2 // indirect
- github.com/julienschmidt/httprouter v1.3.0 // indirect
- github.com/rystaf/go-lemmy v0.0.0-20230623191350-f39e3c8bdcb5 // indirect
- github.com/yuin/goldmark v1.5.4 // indirect
- go.elara.ws/go-lemmy v0.17.3 // indirect
- golang.org/x/text v0.10.0 // indirect
+ github.com/dustin/go-humanize v1.0.1
+ github.com/julienschmidt/httprouter v1.3.0
+ github.com/k3a/html2text v1.2.1
+ github.com/rystaf/go-lemmy v0.0.0-20230720221045-c6d79b98e968
+ github.com/yuin/goldmark v1.5.4
+ golang.org/x/text v0.10.0
+)
+
+require (
+ github.com/cenkalti/backoff/v4 v4.2.0 // indirect
+ github.com/google/go-querystring v1.1.0 // indirect
+ github.com/gorilla/websocket v1.4.2 // indirect
)
diff --git a/go.sum b/go.sum
index 66acff2..984409b 100644
--- a/go.sum
+++ b/go.sum
@@ -2,37 +2,33 @@ github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+M
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
-github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
-github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
-github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
-github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
-github.com/rystaf/go-lemmy v0.0.0-20230622213726-c394de37235c h1:VxOcsDMWaqoBKbhoiSBxPl1zZ62YZ/VAW2nxlBRJiow=
-github.com/rystaf/go-lemmy v0.0.0-20230622213726-c394de37235c/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
-github.com/rystaf/go-lemmy v0.0.0-20230622214853-5f2ab0756865 h1:xitFpcTOSP8RlZWR569yY75B2/7WX08rQQVG+0Mi4SA=
-github.com/rystaf/go-lemmy v0.0.0-20230622214853-5f2ab0756865/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
-github.com/rystaf/go-lemmy v0.0.0-20230622215253-d38b61ec174f h1:EueAC5v+8oX9xK9bT36Tpgbz+c66wUZx5zmyxePurbw=
-github.com/rystaf/go-lemmy v0.0.0-20230622215253-d38b61ec174f/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
-github.com/rystaf/go-lemmy v0.0.0-20230622222647-983d49e1d285 h1:tihBOF3ejTXzYVftaflwqRAXnaY4W9q3iNiE3YMF+D8=
-github.com/rystaf/go-lemmy v0.0.0-20230622222647-983d49e1d285/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
-github.com/rystaf/go-lemmy v0.0.0-20230622230518-ee2cfdf288a4 h1:++T5SoZzghtfNJprWlXiRSpPPdnMSSZgIWWAnPoGx/w=
-github.com/rystaf/go-lemmy v0.0.0-20230622230518-ee2cfdf288a4/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
-github.com/rystaf/go-lemmy v0.0.0-20230623185656-962f9bf8359d h1:ORS2KIBuT+wBn4wJncF1SoLDCVCAUPHASHpQ+Y3TnRI=
-github.com/rystaf/go-lemmy v0.0.0-20230623185656-962f9bf8359d/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
-github.com/rystaf/go-lemmy v0.0.0-20230623191111-7ff8c74b1935 h1:zmzUz6PGRB8yQTT6BRaZNTgNlrk6L7e72dzTnWJTw+I=
-github.com/rystaf/go-lemmy v0.0.0-20230623191111-7ff8c74b1935/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
-github.com/rystaf/go-lemmy v0.0.0-20230623191350-f39e3c8bdcb5 h1:MoI87uid2KqpLdUMZGK2HBOuxJMnPOJaar/4Og2PshM=
-github.com/rystaf/go-lemmy v0.0.0-20230623191350-f39e3c8bdcb5/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
+github.com/k3a/html2text v1.2.1 h1:nvnKgBvBR/myqrwfLuiqecUtaK1lB9hGziIJKatNFVY=
+github.com/k3a/html2text v1.2.1/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA=
+github.com/rystaf/go-lemmy v0.0.0-20230720221045-c6d79b98e968 h1:wfyB6wQzYMH2U8xQvdamExbyCyPhe4o8HP47FMZM5Jk=
+github.com/rystaf/go-lemmy v0.0.0-20230720221045-c6d79b98e968/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-go.elara.ws/go-lemmy v0.17.3 h1:644k23BS2xqKJHJ9cHd8eyt1INpb5myqsBQQL2chBiA=
-go.elara.ws/go-lemmy v0.17.3/go.mod h1:rurQND/HT3yWfX/T4w+hb6vEwRAeAlV+9bSGFkkx5rA=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/main.go b/main.go
index cdd4cdc..95e0f3d 100644
--- a/main.go
+++ b/main.go
@@ -7,12 +7,14 @@ import (
"log"
"net"
"net/http"
+ "os"
"github.com/julienschmidt/httprouter"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
)
+var version string
var watch = flag.Bool("w", false, "watch for file changes")
var addr = flag.String("addr", ":80", "http service address")
var md goldmark.Markdown
@@ -48,10 +50,13 @@ func NewAddHeaderTransport(remoteAddr string) *AddHeaderTransport {
}
func init() {
- md = goldmark.New(goldmark.WithExtensions(extension.Linkify))
+ md = goldmark.New(goldmark.WithExtensions(
+ extension.Linkify,
+ extension.Table,
+ ))
templates = make(map[string]*template.Template)
if !*watch {
- for _, name := range []string{"index.html", "login.html", "frontpage.html", "root.html", "settings.html"} {
+ for _, name := range []string{"index.html", "login.html", "frontpage.html", "root.html", "settings.html", "xhr.html", "create_comment.html"} {
t := template.New(name).Funcs(funcMap)
glob, err := t.ParseGlob("templates/*")
if err != nil {
@@ -61,6 +66,47 @@ func init() {
templates[name] = glob
}
}
+ if os.Getenv("DEBUG") != "" {
+ test()
+ }
+ if data, err := os.ReadFile("VERSION"); err == nil {
+ version = string(data)
+ }
+}
+func test() {
+ links := [][]string{
+ []string{"https://lemmy.local/u/dude", "/lemmy.local/u/dude", "/u/dude"},
+ []string{"https://lemmy.local/u/dude@lemmy.local", "/lemmy.local/u/dude", "/u/dude"},
+ []string{"/u/dude", "/lemmy.local/u/dude", "/u/dude"},
+ []string{"/u/dude@lemmy.world", "/lemmy.local/u/dude@lemmy.world", "/u/dude@lemmy.world"},
+ []string{"/u/dude@lemmy.local", "/lemmy.local/u/dude", "/u/dude"},
+ []string{"https://lemmy.world/c/dude", "/lemmy.local/c/dude@lemmy.world", "/c/dude@lemmy.world"},
+ []string{"https://lemmy.world/u/dude", "/lemmy.local/u/dude@lemmy.world", "/u/dude@lemmy.world"},
+ []string{"https://lemmy.world/u/dude@lemmy.world", "/lemmy.local/u/dude@lemmy.world", "/u/dude@lemmy.world"},
+ []string{"https://lemmy.world/post/123", "/lemmy.local/post/123@lemmy.world", "/post/123@lemmy.world"},
+ []string{"https://lemmy.world/post/123#123", "https://lemmy.world/post/123#123", "https://lemmy.world/post/123#123"},
+ []string{"/post/123", "/lemmy.local/post/123", "/post/123"},
+ []string{"/comment/123", "/lemmy.local/comment/123", "/comment/123"},
+ []string{"https://lemmy.local/comment/123", "/lemmy.local/comment/123", "/comment/123"},
+ }
+ for _, url := range links {
+ output := LemmyLinkRewrite(`href="`+url[0]+`"`, "lemmy.local", "")
+ success := (output == (`href="` + url[1] + `"`))
+ if !success {
+ fmt.Println("\n!!!! multi instance link rewrite failure !!!!")
+ fmt.Println(url)
+ fmt.Println(output)
+ fmt.Println("")
+ }
+ output = LemmyLinkRewrite(`href="`+url[0]+`"`, ".", "lemmy.local")
+ success = (output == (`href="` + url[2] + `"`))
+ if !success {
+ fmt.Println("\n!!!! single instance link rewrite failure !!!!")
+ fmt.Println(success, url)
+ fmt.Println(output)
+ fmt.Println("")
+ }
+ }
}
func middleware(n httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
diff --git a/public/noscript.css b/public/noscript.css
new file mode 100644
index 0000000..8b24e2a
--- /dev/null
+++ b/public/noscript.css
@@ -0,0 +1,17 @@
+.scripting,
+.expando-button,
+.minimize,
+#showimages,
+#lmc,
+.hidechildren {
+ display: none !important;
+}
+.post .expando .image img {
+ visibility: visible;
+}
+div.pager {
+ display: block;
+}
+.savecomment input[type=file] {
+ display: inline-block;
+}
diff --git a/public/style.css b/public/style.css
index 2537f8b..7522f61 100644
--- a/public/style.css
+++ b/public/style.css
@@ -7,6 +7,12 @@ body.dark {
background-color: #262626;
color: #ddd;
}
+code {
+ word-wrap: break-word;
+}
+summary {
+ cursor: pointer;
+}
.dark a {
color: #8cb3d9;
}
@@ -21,24 +27,38 @@ body.dark {
.clearleft {
clear: left;
}
+.img-blur {
+ filter: blur(10px);
+ -webkit-filter: blur(10px);
+ -moz-filter: blur(10px);
+ -o-filter: blur(10px);
+ -ms-filter: blur(10px);
+ transform: scale(1.03);
+}
.post {
- margin: 6px;
+ margin: 6px 0px;
}
.post .thumb {
height: 52px;
width: 70px;
margin: 0px 4px;
+ position: relative;
+ overflow: hidden;
+ --background-color: #e7e7e7;
+}
+.post .thumb div {
+ height: 52px;
+ width: 70px;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
- --background-color: #e7e7e7;
}
.rank {
color: #c6c6c6;
- font-size: medium;
+ font-size: 14px;
margin-top: 17px;
text-align: right;
- min-width: 19px;
+ min-width: 21px;
}
.dark .post .rank {
color: #646464;
@@ -54,52 +74,67 @@ body.dark {
color: #646464;
}
.comment .score {
- margin-right: 2px;
+ clear:left;
+ margin-right: 4px;
overflow:hidden;
}
.comment .content {
+ line-height: 20px;
overflow:hidden;
+ max-width: 840px;
+}
+.comment.hidden {
+ padding-bottom:5px;
}
.comment.hidden .score {
- display: none;
+ visibility: hidden;
}
.score form.link-btn input {
- display: block;
+ display: inline-block;
color: #b7b7b7;
font-size: 20px;
- line-height: 16px;
+ line-height: 15px;
}
.dark .score form.link-btn input {
color: #646464;
}
-.comment form.link-btn input {
+.comment .score form.link-btn input {
line-height: 17px;
}
.score form.like.link-btn input:first-child, .score .like div {
- color: orangered;
+ color: #3880ff;
}
.score form.dislike.link-btn input:last-child, .score .dislike div {
- color: #8080FF;
+ color: #eb445a;
}
-.score form div {
- position: relative;
- top: 2px;
-}
.title a {
- color: #0000ff;
font-size: medium;
text-decoration: none;
}
+.title a p {
+ display: inline;
+}
+.post.distinguished .title a, .post.announcement .title a,
+.dark .post.distinguished .title a:visited, .post.announcement .title a:visited {
+ color: #228822;
+ font-weight: bold;
+}
.dark .title a {
color: #dedede;
}
+.dark .title a:visited {
+ color: #a6a6a6
+}
.post.deleted .title a {
text-decoration: line-through;
}
[disabled] {
cursor: not-allowed;
}
+#loadmore [disabled], .link-btn [disabled] {
+ cursor: wait;
+}
.post .title {
color: #888;
font-size: 10px;
@@ -128,8 +163,8 @@ body.dark {
.message b {
color: #000;
}
-.meta {
- color: #888;
+.dark .message b {
+ color: #ddd;
}
.dark .meta {
color: #b4b4b4;
@@ -137,10 +172,10 @@ body.dark {
.comment {
font-size: 14px;
- margin: 0px 0px 5px 15px;
+ margin: 0px 0px 5px 0px;
border: 1px solid #e6e6e6;
border-radius: 3px;
- padding: 5px 10px 5px 5px;
+ padding: 10px 10px 0px 7px;
}
.dark .comment {
border-color: #333;
@@ -152,7 +187,6 @@ body.dark {
max-height: 300px;
}
.comment .comment {
- margin-left: 15px;
}
.comment .comment,
.comment .comment .comment .comment,
@@ -183,9 +217,30 @@ body.dark {
}
.comment .meta {
font-size: 10px;
- margin-bottom: 3px;
+ margin-bottom: 6px;
}
-.meta a {
+.preview h3 {
+ background-color: #f0f3fc;
+ border: 0px solid #e6e6e6;
+ border-bottom-width: 1px;
+ color: black;
+ margin: 0px;
+ padding: 10px;
+ font-size: 14px;
+}
+.dark .preview h3 {
+ background-color: #333;
+ border-color: #333;
+ color: #ccc;
+}
+.preview .comment {
+ margin-top: 5px;
+ padding: 0px;
+}
+.preview .comment .content {
+ padding: 5px 10px;
+}
+.meta a, .activity .meta a {
color: #369;
text-decoration: none;
font-size: 10px;
@@ -194,6 +249,13 @@ body.dark {
.dark .meta a {
color: #6a98af;
}
+.comment .meta a.distinguished.admin, .post.distinguished a.admin, .post.announcement a.admin {
+ background-color: #ff0011;
+ color: white;
+ font-weight: bold;
+ border-radius: 3px;
+ padding: 0px 2px;
+}
.meta a:hover {
text-decoration: underline;
}
@@ -204,29 +266,65 @@ body.dark {
border-radius: 3px;
padding: 0px 2px;
}
+.comment .meta a.distinguished {
+ background-color: #228822;
+ color: white;
+ font-weight: bold;
+ border-radius: 3px;
+ padding: 0px 2px;
+}
.commentmenu {
font-size: 16px;
- margin: 0px 0px 10px 10px;
+ margin: 0px 0px 10px 0px;
}
.commentmenu div {
border-top: 1px dotted gray;
font-size: 12px;
}
-.savecomment {
- margin: 10px;
+form.savecomment {
+ margin: 0px 0px 10px 0px;
+ width: 500px;
+}
+.comment > .children > form.savecomment {
+ margin: 0px 0px 10px 20px;
+}
+.comment .children {
+ margin: 5px 0px 10px 15px;
}
.savecomment textarea {
- width: 500px;
+ margin: 5px 0px;
+ width: 100%;
height: 100px;
}
+
+.savecomment .upload label div {
+ display: inline-block;
+ border: 1px solid #ccc;
+ height: 20px;
+ line-height: 20px;
+ width: 32px;
+ position: relative;
+ background-color: #999;
+ color: #000;
+ text-align: center;
+ cursor: pointer;
+}
+.savecomment .upload input {
+ display: none;
+}
+.savecomment .right {
+ float:right;
+}
+.savecomment .right a {
+ line-height: 28px;
+ font-size: 10px;
+}
+
.comment .meta a.minimize {
color: #369;
font-size: 10px;
margin-right: 2px;
}
-.comment.hidden {
- margin-bottom: 10px;
-}
.comment.hidden .meta a {
color: gray;
font-weight: 400;
@@ -235,11 +333,15 @@ body.dark {
font-style: italic;
font-weight: 700;
}
-.comment.hidden .content, .comment.hidden .children {
+.comment.hidden .content, .comment.hidden .children, .comment.hidden .morecomments {
display: none;
}
+.children .morecomments {
+}
.morecomments {
- margin: 10px 0px;
+ height: 20px;
+ clear: left;
+ margin: 0px 0px 10px 0px;
font-size: 10px;
}
.morecomments a {
@@ -250,11 +352,11 @@ body.dark {
.morecomments a:hover {
text-decoration: underline;
}
-.member {
+.member, .block {
display: inline-block;
margin-bottom:3px;
}
-.member input {
+.member input, .block input {
font-size: 10px;
font-weight: bold;
display: inline-block;
@@ -266,10 +368,10 @@ body.dark {
bottom: 1px;
cursor: pointer;
}
-.join input {
+.join input, .block input {
background-color: green;
}
-.leave input {
+.leave input, .block.unblock input {
background-color: #cf6165;
}
.pending input {
@@ -278,9 +380,37 @@ body.dark {
.left {
float: left;
}
+span.nsfw {
+ color: #d10023;
+ font-size: 10px;
+ line-height: 14px;
+ border-radius:3px;
+ border: 1px solid #d10023;
+ padding: 0 4px;
+ display: inline-block;
+ font-weight: 400;
+}
+form.nsfw {
+ text-align: center;
+ margin: 50px auto;
+ width: 650px;
+ font-size: 18px;
+}
+form.nsfw div {
+ font-size: 40px;
+ background-color: #ff575b;
+ display: inline-block;
+ padding: 20px 10px;
+ border-radius: 50%;
+ font-weight: bold;
+ color: white;
+}
.gray {
color: #808080;
}
+.loading {
+ color: red !important;
+}
.error {
color: red;
font-size: 13px;
@@ -303,7 +433,7 @@ body.dark {
top: 1px;
}
.pager {
- margin: 10px;
+ margin: 20px 0px;
}
.pager a {
padding: 1px 4px;
@@ -314,6 +444,21 @@ body.dark {
text-decoration: none;
color: #369;
}
+.pager.hidden {
+ display: none;
+}
+#loadmore {
+ display: none;
+}
+#loadmore, #end {
+ margin: 10px 0px;
+}
+#loadmore.show {
+ display: block;
+}
+#end {
+ visibility: hidden;
+}
.buttons li {
display: inline;
}
@@ -322,8 +467,11 @@ body.dark {
font-size: 10px;
padding: 0;
}
+.comment .textarea {
+ width: 100%;
+}
.comment .buttons {
- margin: 8px 0px 3px 0px;
+ margin: 3px 0px 0px 0px;
}
.comment.hidden .buttons {
display: none;
@@ -333,12 +481,13 @@ body.dark {
border-left: 2px solid #c5c1ad;
padding: 0 8px;
}
-.buttons a, .buttons form input {
+.buttons a, .buttons form input, .comment .buttons form input {
text-decoration: none;
color: #888;
- padding-right: 4px;
+ display: inline-block;
+ margin-right: 5px !important;
}
-.buttons a:hover, .title a:hover, .buttons input:hover {
+.buttons a:hover, .title a:hover, .buttons form input:hover, .comment .buttons form input:hover {
text-decoration: underline;
}
.entry {
@@ -378,29 +527,116 @@ body.dark {
.expando-button:hover{
background-color: #466599;
}
+.expando-button.hidden, .children.hidden .comment, .children.hidden .morecomments {
+ display: none;
+}
+.hidechildren .show {
+ display: none;
+}
+.hidechildren.hidden .show {
+ display: inline;
+}
+.hidechildren.hidden .hide {
+ display: none;
+}
+.hidechildren span {
+ pointer-events: none;
+}
.expando {
display: none;
- max-width: 587px;
+ max-width: 870px;
+ margin-top: 5px;
+ position: relative;
color: #000;
}
+#mycommunities, #settingspopup {
+ background-color: white;
+ border: 1px solid #888;
+ display: none;
+ position: absolute;
+ z-index: 100;
+}
+#mycommunities {
+ top: 17px;
+ padding: 5px 0px;
+ border-width: 0px 1px 1px 0px;
+}
+#mycommunities div {
+ margin: 0px 5px;
+}
+#mycommunities a:first-child {
+ text-align: right;
+}
+#mycommunities a {
+ text-decoration: none;
+ color: #369;
+ text-transform: uppercase;
+ font-size: 9px;
+ display: block;
+ padding: 0px 3px;
+}
+.dark #mycommunities a {
+ color: #8cb3d9;
+}
+.dark #mycommunities a:hover {
+ background-color: #3e3e3e;
+}
+#mycommunities a:hover {
+ background-color: #c7def7;
+}
+#settingspopup {
+ right: 10px;
+ top: 45px;
+}
+#settingspopup form {
+ margin: 0px;
+}
+.dark #settingspopup, .dark #mycommunities {
+ background-color: #262626;
+}
+#settingspopup.open, #mycommunities.open {
+ display: inline-block;
+}
.expando.open{
display: block;
}
-.expando img {
+.expando .embed {
+ text-align: center;
+}
+.expando .image {
+ display: block;
+ overflow: hidden;
+ resize: both;
+ max-width: 578px;
+ margin: 0 auto;
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: top left;
+}
+.expando .image img {
+ visibility: hidden;
max-width: 100%;
}
-.expando > div {
+.expando .md {
background-color: #fafafa;
border: 1px solid #369;
border-radius: 7px;
padding: 5px 10px;
- margin: 5px 0px;
+ margin: 5px auto;
+ max-width: 578px;
font-size: 14px;
overflow: auto;
}
-.dark .expando > div {
+.expando .md img {
+ max-width: 100%;
+}
+.expando.open.showimage .md {
+ display: none;
+}
+.dark .expando .md {
background-color: #262626;
color: #ddd;
+ border-color: #666;
}
.expando p, .comment p, .message p {
margin-top: 0;
@@ -436,24 +672,30 @@ body.dark {
text-align: right;
}
.side {
+ display: none;
margin: 0 auto;
font-size: 12px;
width: 300px;
padding-right: 5px;
margin-bottom: 10px;
}
+.side img, .md img{
+ max-width: 100%;
+}
main {
position: relative;
+ margin: 0px 10px;
}
@media (min-width: 900px) {
.side {
+ display: block;
position: absolute;
top: 0;
right: 0;
}
main {
- padding-right: 310px;
- margin-left: 10px;
+ padding-right: 316px;
+ margin-right: 0px;
}
}
.side form {
@@ -538,7 +780,7 @@ main {
color: white;
}
.dark .create a:hover{
- color: #1496dc;
+ color: #0cbe30;
}
.dark .create input[type=submit],
.dark .search .query input,
@@ -598,8 +840,8 @@ h1, h2 {
margin-left: 36px;
}
nav {
- border-bottom: 1px solid #5f99cf;
- background-color: #cee3f8;
+ border-bottom: 1px solid #00a846;
+ background-color: #9ad59b;
z-index: 99;
margin-bottom: 5px;
position: relative;
@@ -634,16 +876,20 @@ nav .communities a.more {
font-weight: bold;
position: absolute;
right: 0;
+ margin: 0;
+}
+.dark nav .communities a.more {
+ background-color: #cccccc;
}
.orangered, .orangered b {
color: orangered !important;
}
-nav a {
+nav .communities a {
text-decoration: none;
color: black;
}
-nav > a:hover {
+nav .title a:hover {
text-decoration: underline;
}
@@ -656,7 +902,7 @@ nav .title, nav > span {
font-variant: small-caps;
}
.dark nav .title, .dark nav > span {
- color: #8cb3d9;;
+ color: #ececec;
}
nav a.title {
margin-left: 70px;
@@ -687,32 +933,37 @@ nav .icon img {
height:100%;
}
+nav .tabs {
+ overflow-x: auto;
+}
+
nav ul {
- white-space: nowrap;
list-style: none;
margin: 5px 2.5px 0px 2.5px;
padding: 0;
- display: inline;
+ display: inline-block;
vertical-align: bottom;
}
nav li {
+ white-space: nowrap;
margin: 0px 1px;
padding: 0px;
- display: inline;
+ display: inline-block;
+ margin-top: 5px;
font-size: 12px;
font-weight: 700;
}
nav ul a {
+ color: #369;
background-color: #eff7ff;
text-decoration: none;
- color: #369;
padding: 2px 6px 0 6px;
}
.dark nav ul a {
background-color: #262626;
- color: #6a98af;
+ color: #ddd;
}
.selected {
@@ -722,20 +973,23 @@ nav ul a {
nav .selected a {
color: orangered;
background-color: white;
- border: 1px solid #5f99cf;
+ border: 1px solid #00a846;
border-bottom: 1px solid white;
}
.dark nav .selected a {
+ color: #d25a32;
border-bottom: 1px solid #262626;
}
nav .right {
position: absolute;
right: 0px;
- background-color: #EFF7FF;
+ background-color: #EFFFEF;
padding: 4px 6px;
line-height: 12px;
border-bottom-left-radius: 7px;
+ border-bottom-right-radius: 7px;
+ margin-right: 10px;
color: gray;
font-size: 10px;
z-index: 101;
@@ -749,12 +1003,12 @@ nav .right a.mailbox {
top: 4px;
color: gray;
}
-nav .right a, .right input[type=submit] {
+nav .right a, nav .right input[type=submit] {
color: #369;
text-decoration: none;
}
-.dark nav .right a, .dark .right input[type=submit]{
- color: #8cb3d9;
+.dark nav .right a, .dark nav .right input[type=submit]{
+ color: #dadada;
}
nav .right form, .comment form, form.link-btn {
display: inline-block;
@@ -799,10 +1053,16 @@ nav .right form input, .comment .buttons input, form.link-btn input {
padding: 5px 10px;
margin: 5px 10px;
}
+.dark .warning {
+ background-color: #544400;
+}
.highlight {
background-color: #ffc;
padding-left: 5px;
}
+.dark .highlight{
+ background-color: #4c4c4c;
+}
form.create {
width: 520px;
@@ -837,13 +1097,21 @@ form.create input[type=file], form.create select {
content: "*";
color: red;
}
+.preferences {
+ margin: 20px;
+}
.preferences div {
font-size: 13px;
margin: 10px;
}
+.preferences div:last-child label {
+ text-align: left;
+ font-size: 10px;
+}
.preferences label{
display: inline-block;
- width: 100px;
+ width: 150px;
+ margin-right: 5px;
text-align: right;
}
diff --git a/public/utils.js b/public/utils.js
index 4bdffbc..df8c22a 100644
--- a/public/utils.js
+++ b/public/utils.js
@@ -1,50 +1,69 @@
-function request(url, params, callback) {
+function request(url, params, callback, errorcallback = function(){}) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
- if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
- callback(xmlHttp.responseText);
+ if (xmlHttp.readyState != 4 ) { return }
+ if (xmlHttp.status == 200) {
+ return callback(xmlHttp.responseText);
+ }
+ errorcallback(xmlHttp.responseText);
}
var method = "GET"
if (params) method = "POST"
xmlHttp.open(method, url, true);
- if (method = "POST")
- xmlHttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xmlHttp.send(params);
}
function postClick(e) {
- console.log(e)
e = e || window.event;
if (e.target.className.indexOf("expando-button") == -1) { return }
var targ = e.currentTarget || e.srcElement || e;
if (targ.nodeType == 3) targ = targ.parentNode;
var bdy = targ.getElementsByClassName("expando")[0]
var btn = targ.getElementsByClassName("expando-button")[0]
- console.log(bdy.style.display)
- console.log(bdy.style.display.indexOf("block"))
if (bdy.className.indexOf("open")>-1) {
bdy.className = 'expando';
btn.className = "expando-button"
+ targ.getElementsByClassName("embed")[0].innerHTML = ""
} else {
bdy.className = 'expando open';
btn.className = "expando-button open"
+ var url = targ.getElementsByClassName("url")[0].href
+ if (id = parseYoutube(url)) {
+ targ.getElementsByClassName("embed")[0].innerHTML = youtubeIframe(id)
+ }
}
}
+function uptil (el, f) {
+ if (el) return f(el) ? el : uptil(el.parentNode, f)
+}
function commentClick(e) {
e = e || window.event;
var targ = e.currentTarget || e.srcElement || e;
if (targ.nodeType == 3) targ = targ.parentNode;
- if (e.target.name=="vote") {
+ if (e.target.name=="submit") {
e.preventDefault()
- var form = e.target.parentNode
+ var form = uptil(e.target, function(el){ return el.tagName == "FORM" })
if (form) {
data = new FormData(form)
- if (("c"+data.get("commentid")) != targ.id) { return }
- params = new URLSearchParams(data).toString()
- params += "&" + e.target.name + "=" + e.target.value
- params += "&xhr=1"
- request(targ.target, params, function(res){
- targ.outerHTML = res
- })
+ data.set(e.target.name, e.target.value)
+ data.set("xhr", 1)
+ if (("c"+data.get("commentid")) == targ.id) {
+ targ.action = form.action
+ if (e.target.value == "preview") {
+ targ = form
+ }
+ console.log("ok")
+ } else if (("c"+data.get("parentid")) == targ.id) {
+ targ = form
+ } else { return }
+ e.target.disabled = "disabled"
+ request(targ.action || "", data,
+ function(res){
+ targ.outerHTML = res
+ setup()
+ },
+ function(res){
+ e.target.disabled = ""
+ })
}
return false
}
@@ -62,8 +81,21 @@ function commentClick(e) {
}
return false
}
- if ((e.target.className.indexOf("loadmore") != -1) ||
- (e.target.className.indexOf("edit") != -1) ||
+ if (e.target.className.indexOf("hidechildren") != -1) {
+ if (e.target.getAttribute("for") != targ.id) { return }
+ e.preventDefault()
+ var btn = targ.getElementsByClassName("hidechildren")[0]
+ var children = targ.getElementsByClassName("children")[0]
+ if (children.className.indexOf("hidden") == -1) {
+ children.className = "children hidden"
+ btn.className = "hidechildren hidden"
+ } else {
+ children.className = "children"
+ btn.className = "hidechildren"
+ }
+ return false
+ }
+ if ((e.target.className.indexOf("edit") != -1) ||
(e.target.className.indexOf("source") != -1) ||
(e.target.className.indexOf("reply") != -1)) {
var id = targ.id
@@ -71,21 +103,341 @@ function commentClick(e) {
e.preventDefault()
request(e.target.href+"&xhr",false, function(res){
targ.outerHTML = res
+ setup()
+ })
+ return false
+ }
+ if (e.target.className.indexOf("loadmore") != -1) {
+ var id = targ.id
+ if (e.target.getAttribute("for") != id) { return }
+ e.preventDefault()
+ var comments = targ.getElementsByClassName("comment")
+ var skip = []
+ for (var i = 0; i < comments.length; i++) {
+ skip.push(comments[i].id)
+ }
+ request(e.target.href+"&xhr",false, function(res){
+ var parent = e.target.parentNode
+ parent.innerHTML = res
+ parent.innerHTML = parent.getElementsByClassName("children")[0].innerHTML
+ var comments = parent.getElementsByClassName("comment")
+ for (var i = 0; i < skip.length; i++) {
+ for (var c = 0; c < comments.length; c++) {
+ if (skip[i] == comments[c].id) {
+ comments[c].remove()
+ }
+ }
+ }
+ parent.outerHTML = parent.innerHTML
+ setup()
})
return false
}
}
+
+function loadMoreComments(e) {
+ e.preventDefault()
+ page = e.target.getAttribute("data-page")
+ var urlParams = new URLSearchParams(window.location.search);
+ urlParams.set("xhr", "1")
+ urlParams.set("page", page)
+ e.target.innerHTML = "loading"
+ e.target.className = "loading"
+ request(window.location.origin+window.location.pathname+"?"+urlParams.toString(), "",
+ function(res){
+ if (res.trim()) {
+ e.target.parentNode.outerHTML = res + '
'
+ setup()
+ } else {
+ e.target.parentNode.innerHTML = ""
+ }
+ }, function() {
+ e.target.innerHTML = "loading failed"
+ })
+ return false;
+}
+function loadMore(e) {
+ e.preventDefault()
+ page = e.target.getAttribute("data-page")
+ e.target.disabled="disabled"
+ e.target.value="loading"
+ var urlParams = new URLSearchParams(window.location.search);
+ urlParams.set("xhr", "1")
+ urlParams.set("page", page)
+ request(window.location.origin+window.location.pathname+"?"+urlParams.toString(), "",
+ function(res){
+ if (res.trim()) {
+ e.target.outerHTML = res + 'view all » '
+ mycommunities.innerHTML += res
+ }, function() {
+ mycommunities.className = ""
+ })
+ }
+ return false
+}
+
+function openSettings(e) {
+ e.preventDefault()
+ var settings = document.getElementById("settingspopup")
+ if (settings.className == "open") {
+ settings.className = ""
+ return false
+ }
+ settings.className = "open"
+ request(e.target.href + "?xhr=1", "", function(res) {
+ settings.innerHTML = res
+ var options = document.getElementsByClassName("scripting")
+ for (var i = 0; i < options.length; i++) {
+ var input = options[i].getElementsByTagName('input')
+ if (!input.length) { continue }
+ if (localStorage.getItem(input[0].name) == "true") {
+ input[0].checked = "checked"
+ }
+ }
+ document.getElementById("settings").addEventListener("submit", saveSettings)
+ document.getElementById("closesettings").addEventListener("click", closeSettings)
})
return false
}
+
+function closeSettings(e) {
+ e.preventDefault()
+ var settings = document.getElementById("settingspopup")
+ settings.className = ""
+ return false
+}
+
+function saveSettings(e) {
+ e = e || window.event;
+ var targ = e.currentTarget || e.srcElement || e;
+ var data = new FormData(targ)
+ e.preventDefault()
+ request(targ.target, data, function(res) {
+ ["endlessScrolling", "autoLoad"].map(function(x) {
+ localStorage.setItem(x, data.get(x)=="on")
+ })
+ window.location.reload()
+ })
+ return false;
+}
+
+function parseYoutube(url){
+ if (url.indexOf("youtu") == -1) return false
+ var regExp = /^.*(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/|shorts\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/;
+ var match = url.match(regExp);
+ if (match && match.length > 1) {
+ return match[1]
+ }
+ return false
+}
+function youtubeIframe(id) {
+ return '
VIDEO '
+}
+
+function showImages(e) {
+ e = e || window.event;
+ e.preventDefault()
+ var targ = e.currentTarget || e.srcElement || e;
+ var parent = targ.parentNode
+ if (parent.className == "") {
+ parent.className = "selected"
+ toggleImages(true)
+ } else {
+ parent.className = ""
+ toggleImages(false)
+ }
+ return false
+}
+
+function toggleImages(open) {
+ var posts = document.getElementsByClassName("post")
+ for (var i = 0; i < posts.length; i++) {
+ var btn = posts[i].getElementsByClassName("expando-button")[0]
+ if (btn.className.indexOf("hidden") != -1) { continue }
+ var img = posts[i].getElementsByClassName("image")
+ if (!img.length) { continue }
+ var bdy = posts[i].getElementsByClassName("expando")[0]
+ if (open) {
+ bdy.className = 'expando open showimage';
+ btn.className = "expando-button open"
+ } else {
+ bdy.className = 'expando';
+ btn.className = "expando-button"
+ }
+ }
+}
+
+function insertImg(e) {
+ e = e || window.event;
+ var form = uptil(e.target, function(el){ return el.tagName == "FORM" })
+ form.querySelector("input[value=preview]").click()
+ var inputs = form.getElementsByTagName("input")
+ for (var i = 0; i < inputs.length; i++) {
+ inputs[i].disabled = "disabled"
+ }
+}
+
+function setup() {
+ if (showimages = document.getElementById("se")) {
+ showimages.addEventListener("click", showImages)
+ }
+ if (settings = document.getElementById("opensettings")) {
+ settings.addEventListener("click", openSettings)
+ }
+ if (settings = document.getElementById("openmycommunities")) {
+ settings.addEventListener("click", toggleMyCommunities)
+ }
+ if (hidechildren = document.getElementById("hidechildren")){
+ hidechildren.addEventListener("click", hideAllChildComments)
+ }
+ if (lmc = document.getElementById("lmc")){
+ var pager = document.getElementsByClassName("pager")
+ if (pager.length) {
+ pager[0].style.display = "none";
+ }
+ lmc.addEventListener("click", loadMoreComments)
+ }
+ var imgUpload = document.getElementsByClassName("imgupload")
+ for (var i = 0; i < imgUpload.length; i++) {
+ imgUpload[i].addEventListener("change", insertImg)
+ }
+ var posts = document.getElementsByClassName("post")
+ for (var i = 0; i < posts.length; i++) {
+ posts[i].addEventListener("click", postClick)
+ var forms = posts[i].getElementsByClassName("link-btn")
+ for (var f = 0; f < forms.length; f++) {
+ forms[f].addEventListener("submit", formSubmit)
+ }
+ var url = posts[i].getElementsByClassName("url")[0].href
+ if (id = parseYoutube(url)) {
+ var btn = posts[i].getElementsByClassName("expando-button")[0]
+ if (btn.className.indexOf("open") > -1) {
+ posts[i].getElementsByClassName("embed")[0].innerHTML = youtubeIframe(id)
+ } else {
+ btn.className = "expando-button"
+ }
+ }
+ }
+ var comments = document.getElementsByClassName("comment")
+ for (var i = 0; i < comments.length; i++) {
+ comments[i].addEventListener("click", commentClick)
+ }
+}
+setup()
+
+if (localStorage.getItem("endlessScrolling") == "true") {
+ var pager = document.getElementsByClassName("pager")
+ if (pager.length) pager[0].className = "pager hidden"
+ var loadmore = document.getElementById("loadmore")
+ if (loadmore) {
+ loadmore.className = "show"
+ loadmore.addEventListener("click", loadMore)
+ }
+}
+if (localStorage.getItem("autoLoad") == "true") {
+ window.onscroll = function(e) {
+ if ((window.innerHeight + Math.round(window.scrollY)) >= document.body.offsetHeight) {
+ if (localStorage.getItem("endlessScrolling") == "true") {
+ if (loadmore = document.getElementById("loadmore")) {
+ loadmore.click()
+ }
+ }
+ if (lmc = document.getElementById("lmc")) {
+ lmc.click()
+ }
+ }
+ };
+}
+
+// delete cookies without HTTPOnly
+var cookies = document.cookie.split(";");
+for (var i = 0; i < cookies.length; i++) {
+ var cookie = cookies[i];
+ var eqPos = cookie.indexOf("=");
+ var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
+ document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;SameSite=None;Secure";
+}
diff --git a/public/ws.js b/public/ws.js
new file mode 100644
index 0000000..08ac7c3
--- /dev/null
+++ b/public/ws.js
@@ -0,0 +1,33 @@
+var ok = false
+var t = 800
+var ws
+var prot = location.protocol == 'https:' ? "wss://":"ws://"
+let port = ":8009"
+
+function start(){
+ let url = prot + document.location.hostname + port + document.location.pathname + document.location.search;
+ console.log('connecting to ', url);
+ ws = new WebSocket(url);
+ ws.onopen = function(){
+ console.log("open");
+ t = 800
+ }
+ ws.onmessage = function(msg){
+ console.log("reload:", msg.data)
+ if (msg.data == "") {
+ return
+ }
+ window.location.reload()
+ }
+ ws.onclose = function(){
+ console.log("close");
+ setTimeout(function(){
+ //start()
+ if (t < 10 * 1000) t += 200
+ }, t);
+ };
+}
+console.log("ws");
+if (typeof WebSocket != 'undefined') {
+ start()
+}
diff --git a/routes.go b/routes.go
index 94253a8..d72d653 100644
--- a/routes.go
+++ b/routes.go
@@ -18,6 +18,7 @@ import (
"github.com/dustin/go-humanize"
"github.com/julienschmidt/httprouter"
+ "github.com/k3a/html2text"
"github.com/rystaf/go-lemmy"
"github.com/rystaf/go-lemmy/types"
"golang.org/x/text/language"
@@ -31,12 +32,12 @@ var funcMap = template.FuncMap{
}
return host
},
- "proxy": func(s string) string {
+ "localize": func(s string) string {
u, err := url.Parse(s)
if err != nil {
return s
}
- return "/" + u.Host + u.Path
+ return "." + u.Path + "@" + u.Host
},
"printer": func(n any) string {
p := message.NewPrinter(language.English)
@@ -110,31 +111,120 @@ var funcMap = template.FuncMap{
}
return false
},
+ "thumbnail": func(p types.Post) string {
+ if p.ThumbnailURL.IsValid() {
+ return p.ThumbnailURL.String() + "?format=jpg&thumbnail=96"
+ }
+ re := regexp.MustCompile(`\/pictrs\/image\/([a-z0-9\-]+)\.([a-z]+)$`)
+ if re.MatchString(p.URL.String()) {
+ return p.URL.String() + "?format=jpg&thumbnail=96"
+ }
+ re = regexp.MustCompile(`^https:\/\/(i\.)?imgur.com\/([a-zA-Z0-9]{5,})(\.[a-zA-Z0-9]+)?`)
+ if re.MatchString(p.URL.String()) {
+ return re.ReplaceAllString(p.URL.String(), "https://i.imgur.com/${2}s.jpg")
+ }
+ if p.URL.IsValid() {
+ return "/_/static/link.png"
+ }
+ return "/_/static/text.png"
+ },
"humanize": humanize.Time,
"markdown": func(host string, body string) template.HTML {
var buf bytes.Buffer
+ re := regexp.MustCompile(`\s---\s`)
+ body = re.ReplaceAllString(body, "\n***\n")
+ // community bangs
+ body = RegReplace(body, `([^\[])!([a-zA-Z0-9_]+)@([a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)+)`, `$1[!$2@$3](/c/$2@$3)`)
if err := md.Convert([]byte(body), &buf); err != nil {
- panic(err)
+ fmt.Println(err)
+ return template.HTML(body)
}
- converted := buf.String()
- converted = strings.Replace(converted, `
$1 $2")
+ return template.HTML(body)
+ },
+ "rmmarkdown": func(body string) string {
+ var buf bytes.Buffer
+ if err := md.Convert([]byte(body), &buf); err != nil {
+ fmt.Println(err)
+ return body
}
- return template.HTML(converted)
+ text := html2text.HTML2TextWithOptions(buf.String(), html2text.WithLinksInnerText())
+ re := regexp.MustCompile(`\<(https?:\/\/|mailto)(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)\>`)
+ return re.ReplaceAllString(text, "")
},
"contains": strings.Contains,
"sub": func(a int32, b int) int {
return int(a) - b
},
+ "add": func(a int32, b int) int {
+ return int(a) + b
+ },
+}
+
+func LemmyLinkRewrite(input string, host string, lemmy_domain string) (body string) {
+ body = input
+ // localize community and user links
+ body = RegReplace(body, `href="https:\/\/([a-zA-Z0-9\.\-]+)\/((c|u|comment|post)\/[^#\?]*?)"`, `href="/$2@$1"`)
+ // remove extra instance tag
+ body = RegReplace(body, `href="(https:\/)?(\/[a-zA-Z0-9\.\-]+)?\/((c|u)\/[a-zA-Z0-9]+@[a-zA-Z0-9\.\-]+)@([a-zA-Z0-9\.\-]+)"`, `href="/$3"`)
+ if lemmy_domain == "" {
+ // add domain to relative links
+ body = RegReplace(body, `href="\/((c|u|post|comment)\/(.*?)")`, `href="/`+host+`/$1`)
+ // convert links to relative
+ body = RegReplace(body, `href="https:\/\/([a-zA-Z0-9\.\-]+\/((c|u|post|comment)\/[a-zA-Z0-9]+"))`, `href="/$1`)
+ } else {
+ // convert local links to relative
+ body = RegReplace(body, `href="https:\/\/`+lemmy_domain+`\/(c\/[a-zA-Z0-9]+"|(c|u|post|comment)\/(.*?)")`, `href="/$1`)
+ body = RegReplace(body, `href="(.*)@`+lemmy_domain+`"`, `href="$1"`)
+ }
+
+ re := regexp.MustCompile(`href="\/?([a-zA-Z0-9\.\-]*)\/(c|u|post|comment)\/(.*?)@(.*?)"`)
+ // assume "old." subdomain is mlmym and remove
+ matches := re.FindAllStringSubmatch(body, -1)
+ for _, match := range matches {
+ if match[4][0:4] == "old." {
+ s := 1
+ if match[1] == "" {
+ s += 1
+ }
+ body = strings.Replace(body, match[0], `href="/`+strings.Join(match[s:4], "/")+"@"+match[4][4:]+`"`, -1)
+ }
+ }
+ // remove redundant instance tag
+ matches = re.FindAllStringSubmatch(body, -1)
+ for _, match := range matches {
+ if match[1] == match[4] {
+ body = strings.Replace(body, match[0], `href="/`+strings.Join(match[1:4], "/")+`"`, -1)
+ }
+ }
+ return body
+}
+
+func RegReplace(input string, match string, replace string) string {
+ re := regexp.MustCompile(match)
+ return re.ReplaceAllString(input, replace)
+}
+
+func getenv(key, fallback string) string {
+ value := os.Getenv(key)
+ if len(value) == 0 {
+ return fallback
+ }
+ return value
}
func Initialize(Host string, r *http.Request) (State, error) {
state := State{
- Host: Host,
- Page: 1,
- Status: http.StatusOK,
+ Host: Host,
+ Page: 1,
+ Status: http.StatusOK,
+ Version: version,
+ }
+ if watch != nil {
+ state.Watch = *watch
}
lemmyDomain := os.Getenv("LEMMY_DOMAIN")
if lemmyDomain != "" {
@@ -157,7 +247,7 @@ func Initialize(Host string, r *http.Request) (State, error) {
token := getCookie(r, "jwt")
user := getCookie(r, "user")
parts := strings.Split(user, ":")
- if len(parts) == 2 {
+ if len(parts) == 2 && token != "" {
if id, err := strconv.Atoi(parts[1]); err == nil {
state.Client.Token = token
sess := Session{
@@ -169,13 +259,28 @@ func Initialize(Host string, r *http.Request) (State, error) {
}
state.Listing = getCookie(r, "DefaultListingType")
state.Sort = getCookie(r, "DefaultSortType")
- state.Dark = getCookie(r, "Dark") != ""
+ state.CommentSort = getCookie(r, "DefaultCommentSortType")
+ if dark := getCookie(r, "Dark"); dark != "" {
+ state.Dark = dark != "0"
+ } else {
+ state.Dark = os.Getenv("DARK") != ""
+ }
+ state.ShowNSFW = getCookie(r, "ShowNSFW") != ""
+ state.HideInstanceNames = getCookie(r, "HideInstanceNames") != ""
+ if hide := getCookie(r, "HideThumbnails"); hide != "" {
+ state.HideThumbnails = hide != "0"
+ } else {
+ state.HideThumbnails = os.Getenv("HIDE_THUMBNAILS") != ""
+ }
state.ParseQuery(r.URL.RawQuery)
if state.Sort == "" {
- state.Sort = "Hot"
+ state.Sort = getenv("SORT", "Hot")
+ }
+ if state.CommentSort == "" {
+ state.CommentSort = getenv("COMMENT_SORT", "Hot")
}
if state.Listing == "" || state.Session == nil && state.Listing == "Subscribed" {
- state.Listing = "All"
+ state.Listing = getenv("LISTING", "All")
}
return state, nil
}
@@ -210,6 +315,8 @@ func Render(w http.ResponseWriter, templateName string, state State) {
if state.Status != http.StatusOK {
w.WriteHeader(state.Status)
}
+ header := w.Header()
+ header.Set("Content-Security-Policy", "script-src 'self'")
err = tmpl.Execute(w, state)
if err != nil {
fmt.Println("execute fail", err)
@@ -219,6 +326,7 @@ func Render(w http.ResponseWriter, templateName string, state State) {
}
func GetRoot(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
data := make(map[string]any)
+ data["Title"] = r.Host
tmpl, err := GetTemplate("root.html")
if err != nil {
fmt.Println("execute fail", err)
@@ -334,7 +442,49 @@ func GetFrontpage(w http.ResponseWriter, r *http.Request, ps httprouter.Params)
if state.Op == "" {
state.GetPosts()
}
- Render(w, "frontpage.html", state)
+ if state.XHR {
+ Render(w, "xhr.html", state)
+ } else {
+ Render(w, "frontpage.html", state)
+ }
+}
+
+func GetCommunities(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+ r.URL.Path = "/search"
+ if ps.ByName("host") != "" {
+ r.URL.Path = "/" + ps.ByName("host") + "/search"
+ }
+ r.URL.RawQuery = "searchtype=Communities&sort=TopMonth"
+ http.Redirect(w, r, r.URL.String(), 301)
+}
+
+func ResolveId(r *http.Request, class string, id string, host string) string {
+ remoteAddr := r.RemoteAddr
+ if r.Header.Get("CF-Connecting-IP") != "" {
+ remoteAddr = r.Header.Get("CF-Connecting-IP")
+ }
+ client := http.Client{Transport: NewAddHeaderTransport(remoteAddr)}
+ c, err := lemmy.NewWithClient("https://"+host, &client)
+ if err != nil {
+ return ""
+ }
+ idn, _ := strconv.Atoi(id)
+ if class == "post" {
+ resp, err := c.Post(context.Background(), types.GetPost{
+ ID: types.NewOptional(idn),
+ })
+ if err != nil {
+ return ""
+ }
+ return resp.PostView.Post.ApID
+ }
+ resp, err := c.Comment(context.Background(), types.GetComment{
+ ID: idn,
+ })
+ if err != nil {
+ return ""
+ }
+ return resp.CommentView.Comment.ApID
}
func GetPost(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
@@ -343,11 +493,40 @@ func GetPost(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
Render(w, "index.html", state)
return
}
+ if path := strings.Split(ps.ByName("postid"), "@"); len(path) > 1 {
+ apid := ResolveId(r, "post", path[0], path[1])
+ if apid != "" {
+ resp, err := state.Client.ResolveObject(context.Background(), types.ResolveObject{
+ Q: apid,
+ })
+ if err != nil {
+ dest := apid
+ if os.Getenv("LEMMY_DOMAIN") == "" {
+ dest = RegReplace(dest, `https:\/\/([a-zA-Z0-9\.\-]+\/post\/\d+)`, `/$1`)
+ }
+ http.Redirect(w, r, dest, 302)
+ return
+ }
+ post, _ := resp.Post.Value()
+ if post.Post.ID > 0 {
+ dest := RegReplace(r.URL.String(), `(([a-zA-Z0-9\.\-]+)?/post/)([a-zA-Z0-9\-\.@]+)`, `$1`)
+ dest += strconv.Itoa(post.Post.ID)
+ http.Redirect(w, r, dest, 302)
+ return
+ } else {
+ http.Redirect(w, r, apid, 302)
+ return
+ }
+ }
+ }
m, _ := url.ParseQuery(r.URL.RawQuery)
if len(m["edit"]) > 0 {
state.Op = "edit_post"
state.GetSite()
}
+ if len(m["content"]) > 0 {
+ state.Content = m["content"][0]
+ }
postid, _ := strconv.Atoi(ps.ByName("postid"))
state.GetPost(postid)
state.GetComments()
@@ -359,6 +538,32 @@ func GetComment(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
Render(w, "index.html", state)
return
}
+ if path := strings.Split(ps.ByName("commentid"), "@"); len(path) > 1 {
+ apid := ResolveId(r, "comment", path[0], path[1])
+ if apid != "" {
+ resp, err := state.Client.ResolveObject(context.Background(), types.ResolveObject{
+ Q: apid,
+ })
+ if err != nil {
+ dest := apid
+ if os.Getenv("LEMMY_DOMAIN") == "" {
+ dest = RegReplace(dest, `https:\/\/([a-zA-Z0-9\.\-]+\/comment\/\d+)`, `/$1`)
+ }
+ http.Redirect(w, r, dest, 302)
+ return
+ }
+ comment, _ := resp.Comment.Value()
+ if comment.Comment.ID > 0 {
+ dest := RegReplace(r.URL.String(), `(([a-zA-Z0-9\.\-]+)?/comment/)([a-zA-Z0-9\-\.@]+)`, `$1`)
+ dest += strconv.Itoa(comment.Comment.ID)
+ http.Redirect(w, r, dest, 302)
+ return
+ } else {
+ http.Redirect(w, r, apid, 302)
+ return
+ }
+ }
+ }
m, _ := url.ParseQuery(r.URL.RawQuery)
if len(m["reply"]) > 0 {
state.Op = "reply"
@@ -366,11 +571,22 @@ func GetComment(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if len(m["edit"]) > 0 {
state.Op = "edit"
}
+ if r.Method == "POST" && len(m["content"]) > 0 {
+ state.Content = m["content"][0]
+ }
if len(m["source"]) > 0 {
state.Op = "source"
}
+ if len(m["context"]) > 0 {
+ ctx, _ := strconv.Atoi(m["context"][0])
+ state.Context = ctx
+ }
commentid, _ := strconv.Atoi(ps.ByName("commentid"))
state.GetComment(commentid)
+ if state.XHR && len(m["content"]) > 0 {
+ Render(w, "create_comment.html", state)
+ return
+ }
state.GetPost(state.PostID)
Render(w, "index.html", state)
}
@@ -461,10 +677,13 @@ func setCookie(w http.ResponseWriter, host string, name string, value string) {
host = ""
}
cookie := http.Cookie{
- Name: name,
- Value: value,
- MaxAge: 86400 * 30,
- Path: "/" + host,
+ Name: name,
+ Value: value,
+ MaxAge: 86400 * 30,
+ HttpOnly: true,
+ SameSite: http.SameSiteNoneMode,
+ Secure: true,
+ Path: "/" + host,
}
http.SetCookie(w, &cookie)
}
@@ -485,23 +704,49 @@ func Settings(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
Render(w, "index.html", state)
return
}
+ state.GetSite()
switch r.Method {
case "POST":
- for _, name := range []string{"DefaultSortType", "DefaultListingType"} {
- setCookie(w, state.Host, name, r.FormValue(name))
+ for _, name := range []string{"DefaultSortType", "DefaultListingType", "DefaultCommentSortType"} {
+ deleteCookie(w, state.Host, name)
+ setCookie(w, "", name, r.FormValue(name))
}
if r.FormValue("darkmode") != "" {
- setCookie(w, state.Host, "Dark", "1")
+ setCookie(w, "", "Dark", "1")
state.Dark = true
} else {
- deleteCookie(w, state.Host, "Dark")
+ setCookie(w, "", "Dark", "0")
state.Dark = false
}
+ if r.FormValue("shownsfw") != "" {
+ setCookie(w, "", "ShowNSFW", "1")
+ state.ShowNSFW = true
+ } else {
+ deleteCookie(w, state.Host, "ShowNSFW")
+ deleteCookie(w, "", "ShowNSFW")
+ state.ShowNSFW = false
+ }
+ if r.FormValue("hideInstanceNames") != "" {
+ setCookie(w, "", "HideInstanceNames", "1")
+ state.HideInstanceNames = true
+ } else {
+ deleteCookie(w, "", "HideInstanceNames")
+ state.HideInstanceNames = false
+ }
+ if r.FormValue("hideThumbnails") != "" {
+ setCookie(w, "", "HideThumbnails", "1")
+ state.HideInstanceNames = true
+ } else {
+ setCookie(w, "", "HideThumbnails", "0")
+ state.HideInstanceNames = false
+ }
state.Listing = r.FormValue("DefaultListingType")
state.Sort = r.FormValue("DefaultSortType")
+ state.CommentSort = r.FormValue("DefaultCommentSortType")
+ // TODO save user settings
case "GET":
if state.Session != nil {
- // TODO fetch server settings
+ // TODO fetch user settings
}
}
Render(w, "settings.html", state)
@@ -510,6 +755,7 @@ func Settings(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
func SignUpOrLogin(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
state, err := Initialize(ps.ByName("host"), r)
if err != nil {
+ fmt.Println(err)
Render(w, "index.html", state)
return
}
@@ -517,11 +763,19 @@ func SignUpOrLogin(w http.ResponseWriter, r *http.Request, ps httprouter.Params)
var username string
switch r.FormValue("submit") {
case "log in":
- resp, err := state.Client.Login(context.Background(), types.Login{
+ login := types.Login{
UsernameOrEmail: r.FormValue("username"),
Password: r.FormValue("password"),
- })
+ }
+ if r.FormValue("totp") != "" {
+ login.Totp2faToken = types.NewOptional(r.FormValue("totp"))
+ }
+ resp, err := state.Client.Login(context.Background(), login)
if err != nil {
+ if strings.Contains(fmt.Sprintf("%v", err), "missing_totp_token") {
+ state.Op = "2fa"
+ }
+ fmt.Println(err)
state.Error = err
state.GetSite()
state.GetCaptcha()
@@ -531,6 +785,7 @@ func SignUpOrLogin(w http.ResponseWriter, r *http.Request, ps httprouter.Params)
if resp.JWT.IsValid() {
token = resp.JWT.String()
username = r.FormValue("username")
+ deleteCookie(w, state.Host, "ShowNSFW")
}
case "sign up":
register := types.Register{
@@ -574,17 +829,14 @@ func SignUpOrLogin(w http.ResponseWriter, r *http.Request, ps httprouter.Params)
q.Add("alert", alert)
r.URL.RawQuery = q.Encode()
http.Redirect(w, r, r.URL.String(), 301)
+ return
}
}
if token != "" {
- if err != nil {
- state.Error = err
- state.GetSite()
- state.GetCaptcha()
- Render(w, "login.html", state)
+ state.GetUser(username)
+ if state.User == nil {
return
}
- state.GetUser(username)
setCookie(w, state.Host, "jwt", token)
userid := strconv.Itoa(state.User.PersonView.Person.ID)
setCookie(w, state.Host, "user", state.User.PersonView.Person.Name+":"+userid)
@@ -601,7 +853,7 @@ func GetLogin(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
return
}
state.GetSite()
- if state.Site.SiteView.LocalSite.CaptchaEnabled {
+ if state.Site != nil && state.Site.SiteView.LocalSite.CaptchaEnabled {
state.GetCaptcha()
}
m, _ := url.ParseQuery(r.URL.RawQuery)
@@ -667,22 +919,49 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
CommunityID: communityid,
Follow: true,
})
+ case "block":
+ communityid, _ := strconv.Atoi(r.FormValue("communityid"))
+ state.Client.BlockCommunity(context.Background(), types.BlockCommunity{
+ CommunityID: communityid,
+ Block: true,
+ })
+ case "unblock":
+ communityid, _ := strconv.Atoi(r.FormValue("communityid"))
+ state.Client.BlockCommunity(context.Background(), types.BlockCommunity{
+ CommunityID: communityid,
+ Block: false,
+ })
case "logout":
deleteCookie(w, state.Host, "jwt")
deleteCookie(w, state.Host, "user")
case "login":
- resp, err := state.Client.Login(context.Background(), types.Login{
- UsernameOrEmail: r.FormValue("user"),
- Password: r.FormValue("pass"),
- })
- if err != nil {
- state.Status = http.StatusUnauthorized
+ login := types.Login{
+ UsernameOrEmail: r.FormValue("username"),
+ Password: r.FormValue("password"),
}
- if resp.JWT.IsValid() {
- state.GetUser(r.FormValue("user"))
- setCookie(w, state.Host, "jwt", resp.JWT.String())
- userid := strconv.Itoa(state.User.PersonView.Person.ID)
- setCookie(w, state.Host, "user", state.User.PersonView.Person.Name+":"+userid)
+ if r.FormValue("totp") != "" {
+ login.Totp2faToken = types.NewOptional(r.FormValue("totp"))
+ }
+ resp, err := state.Client.Login(context.Background(), login)
+ if err != nil {
+ if strings.Contains(fmt.Sprintf("%v", err), "missing_totp_token") {
+ state.Op = "2fa"
+ }
+ state.GetSite()
+ if state.Site != nil && state.Site.SiteView.LocalSite.CaptchaEnabled {
+ state.GetCaptcha()
+ }
+ state.Status = http.StatusUnauthorized
+ state.Error = err
+ Render(w, "login.html", state)
+ return
+ } else if resp.JWT.IsValid() {
+ state.GetUser(r.FormValue("username"))
+ if state.User != nil {
+ setCookie(w, state.Host, "jwt", resp.JWT.String())
+ userid := strconv.Itoa(state.User.PersonView.Person.ID)
+ setCookie(w, state.Host, "user", state.User.PersonView.Person.Name+":"+userid)
+ }
}
case "create_community":
state.GetSite()
@@ -759,6 +1038,9 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
fmt.Println(err)
}
case "create_post":
+ if state.CommunityName == "" {
+ state.CommunityName = r.FormValue("communityname")
+ }
state.GetCommunity(state.CommunityName)
state.GetSite()
if state.Community == nil {
@@ -805,6 +1087,7 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
post := types.EditPost{
PostID: postid,
Body: types.NewOptional(r.FormValue("body")),
+ Name: types.NewOptional(r.FormValue("name")),
URL: types.NewOptional(r.FormValue("url")),
}
if r.FormValue("url") == "" {
@@ -835,6 +1118,38 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
state.Error = err
fmt.Println(err)
}
+ case "save_post":
+ postid, _ := strconv.Atoi(r.FormValue("postid"))
+ _, err := state.Client.SavePost(context.Background(), types.SavePost{
+ PostID: postid,
+ Save: r.FormValue("submit") == "save",
+ })
+ if err != nil {
+ fmt.Println(err)
+ }
+ if r.FormValue("xhr") != "" {
+ state.GetPost(postid)
+ state.PostID = 0
+ state.Op = "save_post"
+ state.XHR = true
+ Render(w, "index.html", state)
+ return
+ }
+ case "save_comment":
+ commentid, _ := strconv.Atoi(r.FormValue("commentid"))
+ _, err := state.Client.SaveComment(context.Background(), types.SaveComment{
+ CommentID: commentid,
+ Save: r.FormValue("submit") == "save",
+ })
+ if err != nil {
+ fmt.Println(err)
+ }
+ if r.FormValue("xhr") != "" {
+ state.XHR = true
+ state.GetComment(commentid)
+ Render(w, "index.html", state)
+ return
+ }
case "delete_post":
postid, _ := strconv.Atoi(r.FormValue("postid"))
post := types.DeletePost{
@@ -851,6 +1166,22 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
r.URL.Path = "/" + state.Host + "/c/" + resp.PostView.Community.Name
r.URL.RawQuery = ""
}
+ case "read_post":
+ postid, _ := strconv.Atoi(r.FormValue("postid"))
+ post := types.MarkPostAsRead{
+ PostID: postid,
+ Read: true,
+ }
+ if r.FormValue("submit") == "mark unread" {
+ post.Read = false
+ }
+ _, err := state.Client.MarkPostAsRead(context.Background(), post)
+ if err != nil {
+ fmt.Println(err)
+ } else if r.FormValue("xhr") != "" {
+ w.Write([]byte{})
+ return
+ }
case "vote_post":
var score int16
score = 1
@@ -868,6 +1199,8 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
state.Client.CreatePostLike(context.Background(), post)
if r.FormValue("xhr") != "" {
state.GetPost(postid)
+ state.PostID = 0
+ state.Op = "vote_post"
state.XHR = true
Render(w, "index.html", state)
return
@@ -875,7 +1208,7 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
case "vote_comment":
var score int16
score = 1
- if r.FormValue("vote") != "▲" {
+ if r.FormValue("submit") != "▲" {
score = -1
}
if r.FormValue("undo") == strconv.Itoa(int(score)) {
@@ -902,33 +1235,122 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
parentid, _ := strconv.Atoi(r.FormValue("parentid"))
state.GetComment(parentid)
}
- createComment := types.CreateComment{
- Content: r.FormValue("content"),
- PostID: state.PostID,
- }
- if state.CommentID > 0 {
- createComment.ParentID = types.NewOptional(state.CommentID)
- }
- resp, err := state.Client.CreateComment(context.Background(), createComment)
+ content := r.FormValue("content")
+ file, handler, err := r.FormFile("file")
if err == nil {
- postid := strconv.Itoa(state.PostID)
- commentid := strconv.Itoa(resp.CommentView.Comment.ID)
- r.URL.Path = "/" + state.Host + "/post/" + postid
- r.URL.Fragment = "c" + commentid
- } else {
- fmt.Println(err)
+ pres, err := state.UploadImage(file, handler)
+ if err != nil {
+ state.Error = err
+ Render(w, "index.html", state)
+ return
+ }
+ content += ("")
+ }
+ if r.FormValue("submit") == "save" {
+ createComment := types.CreateComment{
+ Content: content,
+ PostID: state.PostID,
+ }
+ if state.CommentID > 0 {
+ createComment.ParentID = types.NewOptional(state.CommentID)
+ }
+ resp, err := state.Client.CreateComment(context.Background(), createComment)
+ if err == nil {
+ if r.FormValue("xhr") != "" {
+ state.XHR = true
+ state.Comments = nil
+ state.GetComment(resp.CommentView.Comment.ID)
+ Render(w, "index.html", state)
+ return
+ }
+ postid := strconv.Itoa(state.PostID)
+ commentid := strconv.Itoa(resp.CommentView.Comment.ID)
+ r.URL.Path = "/" + state.Host + "/post/" + postid
+ r.URL.Fragment = "c" + commentid
+ } else {
+ fmt.Println(err)
+ }
+ } else if r.FormValue("submit") == "preview" {
+ q := r.URL.Query()
+ q.Set("content", content)
+ q.Set("reply", "")
+ if r.FormValue("xhr") != "" {
+ q.Set("xhr", "1")
+ }
+ r.URL.RawQuery = q.Encode()
+ if ps.ByName("postid") != "" {
+ GetPost(w, r, ps)
+ return
+ }
+ if ps.ByName("commentid") != "" {
+ GetComment(w, r, ps)
+ return
+ }
+ } else if r.FormValue("xhr") != "" {
+ w.Write([]byte{})
+ return
+ }
+ if r.FormValue("submit") == "cancel" {
+ r.URL.RawQuery = ""
}
case "edit_comment":
commentid, _ := strconv.Atoi(r.FormValue("commentid"))
- resp, err := state.Client.EditComment(context.Background(), types.EditComment{
- CommentID: commentid,
- Content: types.NewOptional(r.FormValue("content")),
- })
- if err != nil {
- fmt.Println(err)
- } else {
- commentid := strconv.Itoa(resp.CommentView.Comment.ID)
- r.URL.Fragment = "c" + commentid
+ q := r.URL.Query()
+ content := r.FormValue("content")
+ file, handler, err := r.FormFile("file")
+ if err == nil {
+ pres, err := state.UploadImage(file, handler)
+ if err != nil {
+ state.Error = err
+ Render(w, "index.html", state)
+ return
+ }
+ content += ("")
+ }
+
+ if r.FormValue("submit") == "save" {
+ resp, err := state.Client.EditComment(context.Background(), types.EditComment{
+ CommentID: commentid,
+ Content: types.NewOptional(content),
+ })
+ if err != nil {
+ fmt.Println(err)
+ } else {
+ commentid := strconv.Itoa(resp.CommentView.Comment.ID)
+ r.URL.Fragment = "c" + commentid
+ r.URL.RawQuery = ""
+ }
+ } else if r.FormValue("submit") == "preview" {
+ q.Set("content", content)
+ q.Set("edit", "")
+ if r.FormValue("xhr") != "" {
+ q.Set("xhr", "1")
+ }
+ r.URL.RawQuery = q.Encode()
+ if ps.ByName("commentid") != "" {
+ GetComment(w, r, ps)
+ return
+ }
+ } else if r.FormValue("submit") == "cancel" {
+ if ps.ByName("commentid") != "" {
+ if r.FormValue("xhr") != "" {
+ q.Set("xhr", "1")
+ }
+ r.URL.RawQuery = q.Encode()
+ GetComment(w, r, ps)
+ return
+ }
+ } else if r.FormValue("xhr") != "" {
+ w.Write([]byte{})
+ return
+ }
+ if r.FormValue("xhr") != "" {
+ state.XHR = true
+ state.GetComment(commentid)
+ Render(w, "index.html", state)
+ return
+ }
+ if r.FormValue("submit") == "cancel" {
r.URL.RawQuery = ""
}
case "delete_comment":
@@ -944,6 +1366,12 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
r.URL.Fragment = "c" + commentid
r.URL.RawQuery = ""
}
+ case "shownsfw":
+ if r.FormValue("submit") == "continue" {
+ setCookie(w, "", "ShowNSFW", "1")
+ } else {
+ r.URL.Path = "/" + state.Host
+ }
}
http.Redirect(w, r, r.URL.String(), 301)
}
@@ -958,6 +1386,7 @@ func GetRouter() *httprouter.Router {
router.GET("/:host/search", middleware(Search))
router.POST("/:host/search", middleware(UserOp))
router.GET("/:host/inbox", middleware(Inbox))
+ router.POST("/:host/inbox", middleware(UserOp))
router.GET("/:host/login", middleware(GetLogin))
router.POST("/:host/login", middleware(SignUpOrLogin))
router.GET("/:host/settings", middleware(Settings))
@@ -982,12 +1411,14 @@ func GetRouter() *httprouter.Router {
router.POST("/:host/create_post", middleware(UserOp))
router.GET("/:host/create_community", middleware(GetCreateCommunity))
router.POST("/:host/create_community", middleware(UserOp))
+ router.GET("/:host/communities", middleware(GetCommunities))
} else {
router.ServeFiles("/_/static/*filepath", http.Dir("public"))
router.GET("/", middleware(GetFrontpage))
router.GET("/search", middleware(Search))
router.POST("/search", middleware(UserOp))
router.GET("/inbox", middleware(Inbox))
+ router.POST("/inbox", middleware(UserOp))
router.GET("/login", middleware(GetLogin))
router.POST("/login", middleware(SignUpOrLogin))
router.GET("/settings", middleware(Settings))
@@ -1012,6 +1443,7 @@ func GetRouter() *httprouter.Router {
router.POST("/create_post", middleware(UserOp))
router.GET("/create_community", middleware(GetCreateCommunity))
router.POST("/create_community", middleware(UserOp))
+ router.GET("/communities", middleware(GetCommunities))
}
return router
}
diff --git a/screenshot.png b/screenshot.png
deleted file mode 100644
index ca97abe..0000000
Binary files a/screenshot.png and /dev/null differ
diff --git a/screenshot1.png b/screenshot1.png
new file mode 100644
index 0000000..d05c42c
Binary files /dev/null and b/screenshot1.png differ
diff --git a/state.go b/state.go
index c13966c..1e2085a 100644
--- a/state.go
+++ b/state.go
@@ -11,6 +11,8 @@ import (
"mime/multipart"
"net/http"
"net/url"
+ "os"
+ "regexp"
"sort"
"strconv"
"strings"
@@ -33,6 +35,12 @@ func (c *Comment) Submitter() bool {
return c.P.Comment.CreatorID == c.P.Post.CreatorID
}
+func (c *Comment) ParentID() int {
+ path := strings.Split(c.P.Comment.Path, ".")
+ id, _ := strconv.Atoi(path[len(path)-2])
+ return id
+}
+
type Person struct {
types.PersonViewSafe
}
@@ -51,45 +59,85 @@ type Post struct {
}
type Session struct {
- UserName string
- UserID int
+ UserName string
+ UserID int
+ Communities []types.CommunityView
}
type State struct {
- Client *lemmy.Client
- HTTPClient *http.Client
- Session *Session
- Status int
- Error error
- Alert string
- Host string
- CommunityName string
- Community *types.GetCommunityResponse
- TopCommunities []types.CommunityView
- Communities []types.CommunityView
- UnreadCount int64
- Sort string
- Listing string
- Page int
- Parts []string
- Posts []Post
- Comments []Comment
- Activities []Activity
- CommentCount int
- PostID int
- CommentID int
- UserName string
- User *types.GetPersonDetailsResponse
- Now int64
- XHR bool
- Op string
- Site *types.GetSiteResponse
- Query string
- SearchType string
- Captcha *types.CaptchaResponse
- Dark bool
+ Watch bool
+ Version string
+ Client *lemmy.Client
+ HTTPClient *http.Client
+ Session *Session
+ Status int
+ Error error
+ Alert string
+ Host string
+ CommunityName string
+ Community *types.GetCommunityResponse
+ TopCommunities []types.CommunityView
+ Communities []types.CommunityView
+ UnreadCount int64
+ Sort string
+ CommentSort string
+ Listing string
+ Page int
+ Parts []string
+ Posts []Post
+ Comments []Comment
+ Activities []Activity
+ CommentCount int
+ PostID int
+ CommentID int
+ Context int
+ UserName string
+ User *types.GetPersonDetailsResponse
+ Now int64
+ XHR bool
+ Op string
+ Site *types.GetSiteResponse
+ Query string
+ Content string
+ SearchType string
+ Captcha *types.CaptchaResponse
+ Dark bool
+ ShowNSFW bool
+ HideInstanceNames bool
+ HideThumbnails bool
}
+func (s State) Unknown() string {
+ fmt.Println(fmt.Sprintf("%v", s.Error))
+ re := regexp.MustCompile(`(.*?)@(.*?)@`)
+ if strings.Contains(fmt.Sprintf("%v", s.Error), "couldnt_find_community") {
+ matches := re.FindAllStringSubmatch(s.CommunityName+"@", -1)
+ if len(matches) < 1 || len(matches[0]) < 3 {
+ return ""
+ }
+ if matches[0][2] != s.Host {
+ remote := "/" + matches[0][2] + "/c/" + matches[0][1]
+ if os.Getenv("LEMMY_DOMAIN") != "" {
+ remote = "https:/" + remote
+ }
+ return remote
+ }
+ }
+ if strings.Contains(fmt.Sprintf("%v", s.Error), "couldnt_find_that_username_or_email") {
+ matches := re.FindAllStringSubmatch(s.UserName+"@", -1)
+ if len(matches) < 1 || len(matches[0]) < 3 {
+ return ""
+ }
+ if matches[0][2] != s.Host {
+ remote := "/" + matches[0][2] + "/u/" + matches[0][1]
+ if os.Getenv("LEMMY_DOMAIN") != "" {
+ remote = "https:/" + remote
+ }
+ return remote
+ }
+ }
+ return ""
+}
func (p State) SortBy(v string) string {
var q string
if p.Query != "" || p.SearchType == "Communities" {
@@ -155,6 +203,7 @@ func (state *State) ParseQuery(RawQuery string) {
}
if len(m["sort"]) > 0 {
state.Sort = m["sort"][0]
+ state.CommentSort = m["sort"][0]
}
if len(m["communityname"]) > 0 {
state.CommunityName = m["communityname"][0]
@@ -168,6 +217,11 @@ func (state *State) ParseQuery(RawQuery string) {
if len(m["xhr"]) > 0 {
state.XHR = true
}
+ if len(m["view"]) > 0 {
+ if m["view"][0] == "Saved" {
+ state.Op = "Saved"
+ }
+ }
//if len(m["op"]) > 0 {
// state.Op = m["op"][0]
//}
@@ -208,16 +262,27 @@ func (state *State) GetCaptcha() {
}
}
func (state *State) GetSite() {
- token := state.Client.Token
- state.Client.Token = ""
resp, err := state.Client.Site(context.Background(), types.GetSite{})
if err != nil {
+ fmt.Println(err)
state.Status = http.StatusInternalServerError
- state.Host = ""
+ state.Host = "."
+ state.Error = errors.New("unable to retrieve site")
return
}
- state.Client.Token = token
state.Site = resp
+ if !state.Site.MyUser.IsValid() {
+ return
+ }
+ for _, c := range state.Site.MyUser.MustValue().Follows {
+ state.Session.Communities = append(state.Session.Communities, types.CommunityView{
+ Community: c.Community,
+ Subscribed: "Subscribed",
+ })
+ }
+ sort.Slice(state.Session.Communities, func(a, b int) bool {
+ return state.Session.Communities[a].Community.Name < state.Session.Communities[b].Community.Name
+ })
}
func (state *State) GetComment(commentid int) {
@@ -227,9 +292,9 @@ func (state *State) GetComment(commentid int) {
state.CommentID = commentid
cresp, err := state.Client.Comments(context.Background(), types.GetComments{
ParentID: types.NewOptional(state.CommentID),
- Sort: types.NewOptional(types.CommentSortType(state.Sort)),
+ Sort: types.NewOptional(types.CommentSortType(state.CommentSort)),
Type: types.NewOptional(types.ListingType("All")),
- Limit: types.NewOptional(int64(200)),
+ Limit: types.NewOptional(int64(50)),
})
if err != nil {
fmt.Println(err)
@@ -251,6 +316,33 @@ func (state *State) GetComment(commentid int) {
state.Comments = append(state.Comments, comment)
}
}
+ if len(state.Comments) == 0 {
+ return
+ }
+ ctx, err := state.GetContext(state.Context, state.Comments[0])
+ if err != nil {
+ fmt.Println(err)
+ } else {
+ state.Comments = []Comment{ctx}
+ }
+}
+func (state *State) GetContext(depth int, comment Comment) (ctx Comment, err error) {
+ if depth < 1 || comment.ParentID() == 0 {
+ return comment, nil
+ }
+ cresp, err := state.Client.Comment(context.Background(), types.GetComment{
+ ID: comment.ParentID(),
+ })
+ if err != nil {
+ return
+ }
+ ctx, err = state.GetContext(depth-1, Comment{
+ P: cresp.CommentView,
+ State: state,
+ C: []Comment{comment},
+ ChildCount: comment.ChildCount + 1,
+ })
+ return
}
func (state *State) GetComments() {
if state.Sort != "Hot" && state.Sort != "Top" && state.Sort != "Old" && state.Sort != "New" {
@@ -258,9 +350,9 @@ func (state *State) GetComments() {
}
cresp, err := state.Client.Comments(context.Background(), types.GetComments{
PostID: types.NewOptional(state.PostID),
- Sort: types.NewOptional(types.CommentSortType(state.Sort)),
+ Sort: types.NewOptional(types.CommentSortType(state.CommentSort)),
Type: types.NewOptional(types.ListingType("All")),
- Limit: types.NewOptional(int64(200)),
+ Limit: types.NewOptional(int64(50)),
Page: types.NewOptional(int64(state.Page)),
})
if err != nil {
@@ -343,6 +435,7 @@ func (state *State) GetMessages() {
Post: m.Post,
Creator: m.Creator,
Community: m.Community,
+ Counts: m.Counts,
},
Op: unread,
State: state,
@@ -362,12 +455,14 @@ func (state *State) GetUser(username string) {
limit = 1
}
resp, err := state.Client.PersonDetails(context.Background(), types.GetPersonDetails{
- Username: types.NewOptional(state.UserName),
- Page: types.NewOptional(int64(state.Page)),
- Limit: types.NewOptional(int64(limit)),
+ Username: types.NewOptional(state.UserName),
+ Page: types.NewOptional(int64(state.Page)),
+ Limit: types.NewOptional(int64(limit)),
+ SavedOnly: types.NewOptional(state.Op == "Saved"),
})
if err != nil {
fmt.Println(err)
+ state.Error = err
state.Status = http.StatusInternalServerError
return
}
@@ -425,13 +520,16 @@ func (state *State) MarkAllAsRead() {
}
func (state *State) GetPosts() {
- resp, err := state.Client.Posts(context.Background(), types.GetPosts{
- Sort: types.NewOptional(types.SortType(state.Sort)),
- Type: types.NewOptional(types.ListingType(state.Listing)),
- CommunityName: types.NewOptional(state.CommunityName),
- Limit: types.NewOptional(int64(25)),
- Page: types.NewOptional(int64(state.Page)),
- })
+ posts := types.GetPosts{
+ Sort: types.NewOptional(types.SortType(state.Sort)),
+ Type: types.NewOptional(types.ListingType(state.Listing)),
+ Limit: types.NewOptional(int64(25)),
+ Page: types.NewOptional(int64(state.Page)),
+ }
+ if state.CommunityName != "" {
+ posts.CommunityName = types.NewOptional(state.CommunityName)
+ }
+ resp, err := state.Client.Posts(context.Background(), posts)
if err != nil {
fmt.Println(err)
state.Status = http.StatusInternalServerError
@@ -449,7 +547,18 @@ func (state *State) GetPosts() {
func (state *State) Search(searchtype string) {
if state.Query == "" && searchtype == "Communities" {
+ if state.Listing == "Subscribed" {
+ if state.Page > 1 {
+ return
+ }
+ if state.Site == nil {
+ state.GetSite()
+ }
+ state.Communities = state.Session.Communities
+ return
+ }
resp, err := state.Client.Communities(context.Background(), types.ListCommunities{
+ Type: types.NewOptional(types.ListingType(state.Listing)),
Sort: types.NewOptional(types.SortType(state.Sort)),
Limit: types.NewOptional(int64(25)),
Page: types.NewOptional(int64(state.Page)),
@@ -464,7 +573,7 @@ func (state *State) Search(searchtype string) {
search := types.Search{
Q: state.Query,
Sort: types.NewOptional(types.SortType(state.Sort)),
- ListingType: types.NewOptional(types.ListingType("All")),
+ ListingType: types.NewOptional(types.ListingType(state.Listing)),
Type: types.NewOptional(types.SearchType(searchtype)),
Limit: types.NewOptional(int64(25)),
Page: types.NewOptional(int64(state.Page)),
@@ -492,9 +601,13 @@ func (state *State) Search(searchtype string) {
})
}
for _, c := range resp.Comments {
- state.Comments = append(state.Comments, Comment{
+ comment := Comment{
P: c,
State: state,
+ }
+ state.Activities = append(state.Activities, Activity{
+ Timestamp: c.Comment.Published.Time,
+ Comment: &comment,
})
}
state.Communities = resp.Communities
@@ -512,22 +625,22 @@ func (state *State) GetPost(postid int) {
})
if err != nil {
state.Status = http.StatusInternalServerError
+ state.Error = err
return
- } else {
- state.Posts = []Post{Post{
- PostView: resp.PostView,
- State: state,
- }}
- if state.CommentID > 0 && len(state.Posts) > 0 {
- state.Posts[0].Rank = -1
- }
- state.CommunityName = resp.PostView.Community.Name
- cresp := types.GetCommunityResponse{
- CommunityView: resp.CommunityView,
- Moderators: resp.Moderators,
- }
- state.Community = &cresp
}
+ state.Posts = []Post{Post{
+ PostView: resp.PostView,
+ State: state,
+ }}
+ if state.CommentID > 0 && len(state.Posts) > 0 {
+ state.Posts[0].Rank = -1
+ }
+ state.CommunityName = resp.PostView.Community.Name
+ cresp := types.GetCommunityResponse{
+ CommunityView: resp.CommunityView,
+ Moderators: resp.Moderators,
+ }
+ state.Community = &cresp
}
func (state *State) GetCommunity(communityName string) {
@@ -580,19 +693,19 @@ func (state *State) UploadImage(file multipart.File, header *multipart.FileHeade
func getChildren(parent *Comment, pool []types.CommentView, postCreatorID int) {
var children []Comment
- total := -1
+ total := int32(0)
for _, c := range pool {
levels := strings.Split(c.Comment.Path, ".")
for i, l := range levels {
id, _ := strconv.Atoi(l)
if id == parent.P.Comment.ID {
- total = total + 1
if i == (len(levels) - 2) {
children = append(children, Comment{
P: c,
C: children,
State: parent.State,
})
+ total += c.Counts.ChildCount
}
}
@@ -600,7 +713,8 @@ func getChildren(parent *Comment, pool []types.CommentView, postCreatorID int) {
}
for i, _ := range children {
getChildren(&children[i], pool, postCreatorID)
+ parent.ChildCount += 1
}
parent.C = children
- parent.ChildCount = total
+ parent.P.Counts.ChildCount -= total
}
diff --git a/templates/activities.html b/templates/activities.html
index f05c5a4..110419b 100644
--- a/templates/activities.html
+++ b/templates/activities.html
@@ -3,16 +3,23 @@