fb0c5ead0f5f — Peter Sanchez 3 months ago
Merge tedu upstream
M activity.go +10 -3
@@ 1439,9 1439,6 @@ func jonkjonk(user *WhatAbout, h *Honk) 
 		jo = junk.New()
 		jo["id"] = h.XID
 		jo["type"] = "Note"
-		if h.What == "event" {
-			jo["type"] = "Event"
-		}
 		if h.What == "update" {
 			j["type"] = "Update"
 			jo["updated"] = dt

          
@@ 1555,6 1552,12 @@ func jonkjonk(user *WhatAbout, h *Honk) 
 			jl["href"] = h.Link
 			atts = append(atts, jl)
 		}
+		if tooooFancy(h.Noise) {
+			jo["type"] = "Article"
+		}
+		if h.What == "event" {
+			jo["type"] = "Event"
+		}
 		if len(atts) > 0 {
 			jo["attachment"] = atts
 		}

          
@@ 1615,6 1618,10 @@ func jonkjonk(user *WhatAbout, h *Honk) 
 	return j, jo
 }
 
+func tooooFancy(noise string) bool {
+	return strings.Contains(noise, "<img") || strings.Contains(noise, "<table")
+}
+
 var oldjonks = gencache.New(gencache.Options[string, []byte]{Fill: func(xid string) ([]byte, bool) {
 	row := stmtAnyXonk.QueryRow(xid)
 	honk := scanhonk(row)

          
M admin.go +7 -8
@@ 59,15 59,11 @@ func adminscreen() {
 		},
 	}
 
-	termvc.Start()
-	defer termvc.Restore()
-	go termvc.Catch(nil)
-
 	app := termvc.NewApp()
 	scr := termvc.NewScreen()
 	scr.DefaultColor(35)
 	var tabs []termvc.Element
-	insns := termvc.NewTextLabel("honk admin")
+	insns := termvc.NewStringWrapper("honk admin")
 	tabs = append(tabs, insns)
 
 	for _, m := range messages {

          
@@ 107,9 103,12 @@ func adminscreen() {
 	group.SetFocus(1)
 	group.SetHeight(len(tabs)-1, 1)
 	group.SetHeight(len(tabs)-2, 6)
-	pane := termvc.NewMainPanel(group)
+
+	app.Element = group
+	app.Screen = scr
 
-	app.Element = pane
-	app.Screen = scr
+	termvc.Start()
+	defer termvc.Restore()
+	go termvc.Catch(nil)
 	app.Loop()
 }

          
M cli.go +48 -40
@@ 10,7 10,9 @@ import (
 
 type cmd struct {
 	help     string
+	help2    string
 	callback func(args []string)
+	nargs    int
 }
 
 var commands = map[string]cmd{

          
@@ 40,29 42,41 @@ var commands = map[string]cmd{
 		},
 	},
 	"import": {
-		help: "import data into honk",
+		help:  "import data into honk",
+		help2: "import username honk|mastodon|twitter srcdir",
 		callback: func(args []string) {
-			if len(args) != 4 {
-				errx("import username honk|mastodon|twitter srcdir")
-			}
 			importMain(args[1], args[2], args[3])
 		},
+		nargs: 4,
 	},
 	"export": {
-		help: "export data from honk",
+		help:  "export data from honk",
+		help2: "export username destdir",
 		callback: func(args []string) {
-			if len(args) != 3 {
-				errx("export username destdir")
-			}
 			export(args[1], args[2])
 		},
+		nargs: 3,
+	},
+	"dumpthread": {
+		help:  "export a thread for debugging",
+		help2: "dumpthread user convoy",
+		callback: func(args []string) {
+			dumpthread(args[1], args[2])
+		},
+		nargs: 3,
+	},
+	"rawimport": {
+		help:  "import activity objects for debugging",
+		help2: "rawimport username filename",
+		callback: func(args []string) {
+			rawimport(args[1], args[2])
+		},
+		nargs: 3,
 	},
 	"devel": {
-		help: "turn devel on/off",
+		help:  "turn devel on/off",
+		help2: "devel (on|off)",
 		callback: func(args []string) {
-			if len(args) != 2 {
-				errx("need an argument: devel (on|off)")
-			}
 			switch args[1] {
 			case "on":
 				setconfig("devel", 1)

          
@@ 72,13 86,12 @@ var commands = map[string]cmd{
 				errx("argument must be on or off")
 			}
 		},
+		nargs: 2,
 	},
 	"setconfig": {
-		help: "set honk config",
+		help:  "set honk config",
+		help2: "setconfig key val",
 		callback: func(args []string) {
-			if len(args) != 3 {
-				errx("need an argument: setconfig key val")
-			}
 			var val interface{}
 			var err error
 			if val, err = strconv.Atoi(args[2]); err != nil {

          
@@ 86,6 99,7 @@ var commands = map[string]cmd{
 			}
 			setconfig(args[1], val)
 		},
+		nargs: 3,
 	},
 	"adduser": {
 		help: "add a user to honk",

          
@@ 94,29 108,25 @@ var commands = map[string]cmd{
 		},
 	},
 	"deluser": {
-		help: "delete a user from honk",
+		help:  "delete a user from honk",
+		help2: "deluser username",
 		callback: func(args []string) {
-			if len(args) < 2 {
-				errx("usage: honk deluser username")
-			}
 			deluser(args[1])
 		},
+		nargs: 2,
 	},
 	"chpass": {
-		help: "change password of an account",
+		help:  "change password of an account",
+		help2: "chpass username",
 		callback: func(args []string) {
-			if len(args) < 2 {
-				errx("usage: honk chpass username")
-			}
 			chpass(args[1])
 		},
+		nargs: 2,
 	},
 	"follow": {
-		help: "follow an account",
+		help:  "follow an account",
+		help2: "follow username url",
 		callback: func(args []string) {
-			if len(args) < 3 {
-				errx("usage: honk follow username url")
-			}
 			user, err := butwhatabout(args[1])
 			if err != nil {
 				errx("user %s not found", args[1])

          
@@ 131,13 141,12 @@ var commands = map[string]cmd{
 				followyou(user, honkerid, true)
 			}
 		},
+		nargs: 3,
 	},
 	"unfollow": {
-		help: "unfollow an account",
+		help:  "unfollow an account",
+		help2: "unfollow username url",
 		callback: func(args []string) {
-			if len(args) < 3 {
-				errx("usage: honk unfollow username url")
-			}
 			user, err := butwhatabout(args[1])
 			if err != nil {
 				errx("user not found")

          
@@ 149,13 158,12 @@ var commands = map[string]cmd{
 			}
 			unfollowyou(user, honkerid, true)
 		},
+		nargs: 3,
 	},
 	"sendmsg": {
-		help: "send a raw activity",
+		help:  "send a raw activity",
+		help2: "sendmsg username filename rcpt",
 		callback: func(args []string) {
-			if len(args) < 4 {
-				errx("usage: honk sendmsg username filename rcpt")
-			}
 			user, err := butwhatabout(args[1])
 			if err != nil {
 				errx("user %s not found", args[1])

          
@@ 166,6 174,7 @@ var commands = map[string]cmd{
 			}
 			deliverate(user.ID, args[3], data)
 		},
+		nargs: 4,
 	},
 	"cleanup": {
 		help: "clean up stale data from database",

          
@@ 196,14 205,13 @@ var commands = map[string]cmd{
 		},
 	},
 	"unplug": {
-		help: "disconnect from a dead server",
+		help:  "disconnect from a dead server",
+		help2: "unplug servername",
 		callback: func(args []string) {
-			if len(args) < 2 {
-				errx("usage: honk unplug servername")
-			}
 			name := args[1]
 			unplugserver(name)
 		},
+		nargs: 2,
 	},
 	"backup": {
 		help: "backup honk",

          
M database.go +2 -2
@@ 301,7 301,7 @@ func gethonksbycombo(userid UserID, comb
 	return getsomehonks(rows, err)
 }
 func gethonksbyconvoy(userid UserID, convoy string, wanted int64) []*Honk {
-	rows, err := stmtHonksByConvoy.Query(convoy, wanted, userid)
+	rows, err := stmtHonksByConvoy.Query(convoy, wanted, userid, 1000)
 	return getsomehonks(rows, err)
 }
 func gethonksbysearch(userid UserID, q string, wanted int64) []*Honk {

          
@@ 1181,7 1181,7 @@ func prepareStatements(db *sql.DB) {
 		select xid, convoy from honks, getthread where honks.rid <> '' and honks.rid = getthread.x
 		union
 		select rid, convoy from honks, getthread where honks.xid = getthread.x and rid <> ''
-	) `+selecthonks+"where honks.honkid > ? and honks.userid = ? and xid in (select x from getthread)"+limit)
+	) `+selecthonks+"where honks.honkid > ? and honks.userid = ? and xid in (select x from getthread)"+smalllimit)
 	stmtHonksByOntology = preparetodie(db, selecthonks+"join onts on honks.honkid = onts.honkid where honks.honkid > ? and onts.ontology = ? and (honks.userid = ? or (? = -1 and honks.whofore = 2))"+limit)
 
 	stmtSaveMeta = preparetodie(db, "insert into honkmeta (honkid, genus, json) values (?, ?, ?)")

          
M docs/changelog.txt +2 -0
@@ 2,6 2,8 @@ changelog
 
 ### next
 
++ Another tune up for thread sort.
+
 + Rework setup and admin screens.
 
 + Add some compat for forgefed activities.

          
M fun.go +3 -0
@@ 639,12 639,15 @@ var allhandles = gencache.New(gencache.O
 		dlog.Printf("need to get a handle: %s", xid)
 		info, _, err := investigate(xid)
 		if err != nil {
+			dlog.Printf("failed to get handle: %s", err)
 			m := re_unurl.FindStringSubmatch(xid)
 			if len(m) > 2 {
 				handle = m[2]
 			} else {
 				handle = xid
 			}
+			when := time.Now().UTC().Format(dbtimeformat)
+			savexonker(xid, handle, "handle", when)
 		} else {
 			handle = info.Name
 		}

          
M go.mod +2 -2
@@ 10,8 10,8 @@ require (
 	golang.org/x/net v0.21.0
 	humungus.tedunangst.com/r/go-sqlite3 v1.2.1
 	humungus.tedunangst.com/r/gonix v0.1.4
-	humungus.tedunangst.com/r/termvc v0.1.0
-	humungus.tedunangst.com/r/webs v0.7.12
+	humungus.tedunangst.com/r/termvc v0.1.2
+	humungus.tedunangst.com/r/webs v0.7.15
 )
 
 require (

          
M go.sum +4 -4
@@ 50,7 50,7 @@ humungus.tedunangst.com/r/go-sqlite3 v1.
 humungus.tedunangst.com/r/go-sqlite3 v1.2.1/go.mod h1:YrRIH0O7uePPLbJriXrER44ym5aQ0QxK8CnaT/GWOkg=
 humungus.tedunangst.com/r/gonix v0.1.4 h1:FuvWYQlFIzmfHxfvIfq5SYpSiHhFcpJqq3pi+w45s78=
 humungus.tedunangst.com/r/gonix v0.1.4/go.mod h1:VFBc2bPDXr1ayHOmHUutxYu8fSM+pkwK8o36h4rkORg=
-humungus.tedunangst.com/r/termvc v0.1.0 h1:Xe5ImK7W4jZqAOtZhTiec1Lc7CbNpmC2UMuDBUMcVwk=
-humungus.tedunangst.com/r/termvc v0.1.0/go.mod h1:TnlG9PbH77OpEf46iDyb/H9drjegQNwhpXalmGGrbhU=
-humungus.tedunangst.com/r/webs v0.7.12 h1:SbAOmzwn4LPB5AmAzc1KLIA1zymEkXFyFMV2Cp3MDdo=
-humungus.tedunangst.com/r/webs v0.7.12/go.mod h1:ylhqHSPI0Oi7b4nsnx5mSO7AjLXN7wFpEHayLfN/ugk=
+humungus.tedunangst.com/r/termvc v0.1.2 h1:TPH5ThFRjR+f1ko9Uh4dhm3ieNKvmoPZAvu3tU5lNIE=
+humungus.tedunangst.com/r/termvc v0.1.2/go.mod h1:TnlG9PbH77OpEf46iDyb/H9drjegQNwhpXalmGGrbhU=
+humungus.tedunangst.com/r/webs v0.7.15 h1:97l++EcyhAgCUgBDSUvvNHG1wFW1jAdqlqEbWvbbWik=
+humungus.tedunangst.com/r/webs v0.7.15/go.mod h1:ylhqHSPI0Oi7b4nsnx5mSO7AjLXN7wFpEHayLfN/ugk=

          
M honk.go +4 -0
@@ 200,6 200,10 @@ func (honk *Honk) IsReacted() bool {
 	return honk.Flags&flagIsReacted != 0
 }
 
+func (honk *Honk) ShortXID() string {
+	return shortxid(honk.XID)
+}
+
 type Donk struct {
 	FileID   int64
 	XID      string

          
M import.go +117 -0
@@ 48,6 48,7 @@ func importMain(username, flavor, source
 }
 
 type ActivityObject struct {
+	Id           string
 	AttributedTo string
 	Summary      string
 	Content      string

          
@@ 658,3 659,119 @@ func export(username, file string) {
 	zd.Close()
 	fd.Close()
 }
+
+func dumpthread(username, convoy string) {
+	user, _ := butwhatabout(username)
+	honks := gethonksbyconvoy(user.ID, convoy, 0)
+	var jonks []junk.Junk
+	for _, honk := range honks {
+		noise := honk.Noise
+		j, jo := jonkjonk(user, honk)
+		if honk.Format == "markdown" {
+			source := junk.New()
+			source["mediaType"] = "text/markdown"
+			source["content"] = noise
+			jo["source"] = source
+		}
+		jonks = append(jonks, j)
+	}
+	j := junk.New()
+	j["@context"] = itiswhatitis
+	j["orderedItems"] = jonks
+	j.Write(os.Stdout)
+}
+func rawimport(username, filename string) {
+	user, _ := butwhatabout(username)
+	type Activity struct {
+		Id     string
+		Type   string
+		To     interface{}
+		Cc     []string
+		Object ActivityObject
+	}
+	var outbox struct {
+		OrderedItems []Activity
+	}
+	ilog.Println("Importing honks...")
+	fd, err := os.Open(filename)
+	if err != nil {
+		elog.Fatal(err)
+	}
+	dec := json.NewDecoder(fd)
+	err = dec.Decode(&outbox)
+	if err != nil {
+		elog.Fatalf("error parsing json: %s", err)
+	}
+	fd.Close()
+
+	havetoot := func(xid string) bool {
+		var id int64
+		row := stmtFindXonk.QueryRow(user.ID, xid)
+		err := row.Scan(&id)
+		if err == nil {
+			return true
+		}
+		return false
+	}
+
+	items := outbox.OrderedItems
+	for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 {
+		items[i], items[j] = items[j], items[i]
+	}
+	for _, item := range items {
+		toot := item
+		if toot.Type != "Create" {
+			continue
+		}
+		xid := toot.Object.Id
+		if havetoot(xid) {
+			continue
+		}
+
+		convoy := toot.Object.Context
+		if convoy == "" {
+			convoy = toot.Object.Conversation
+		}
+		var audience []string
+		to, ok := toot.To.(string)
+		if ok {
+			audience = append(audience, to)
+		} else {
+			for _, t := range toot.To.([]interface{}) {
+				audience = append(audience, t.(string))
+			}
+		}
+		content := toot.Object.Content
+		format := "html"
+		if toot.Object.Source.MediaType == "text/markdown" {
+			content = toot.Object.Source.Content
+			format = "markdown"
+		}
+		audience = append(audience, toot.Cc...)
+		honk := Honk{
+			UserID:   user.ID,
+			What:     "honk",
+			Honker:   toot.Object.AttributedTo,
+			XID:      xid,
+			RID:      toot.Object.InReplyTo,
+			Date:     toot.Object.Published,
+			URL:      xid,
+			Audience: audience,
+			Noise:    content,
+			Convoy:   convoy,
+			Whofore:  2,
+			Format:   format,
+			Precis:   toot.Object.Summary,
+		}
+		if !loudandproud(honk.Audience) {
+			honk.Whofore = 3
+		}
+		for _, t := range toot.Object.Tag {
+			switch t.Type {
+			case "Hashtag":
+				honk.Onts = append(honk.Onts, t.Name)
+			}
+		}
+		savehonk(&honk)
+	}
+}

          
M main.go +3 -0
@@ 186,6 186,9 @@ func main() {
 	if !ok {
 		errx("don't know about %q", cmd)
 	}
+	if c.nargs > 0 && len(args) != c.nargs {
+		errx("incorrect arg count: %s", c.help2)
+	}
 
 	c.callback(args)
 }

          
M upgradedb.go +1 -0
@@ 226,6 226,7 @@ func upgradedb() {
 		setV(53)
 		fallthrough
 	case 53:
+		setcsrfkey()
 		try("analyze")
 		closedatabases()
 

          
M util.go +43 -6
@@ 109,7 109,7 @@ func initdb() {
 	t2 := termvc.NewTextArea()
 	group := termvc.NewVStack(t1, form, t2)
 	group.SetFocus(1)
-	app.Element = termvc.NewMainPanel(group)
+	app.Element = group
 	app.Screen = termvc.NewScreen()
 	btn.Submit = func() {
 		t2.Value = ""

          
@@ 147,10 147,7 @@ func initdb() {
 		return
 	}
 
-	var randbytes [16]byte
-	rand.Read(randbytes[:])
-	key := fmt.Sprintf("%x", randbytes)
-	setconfig("csrfkey", key)
+	setcsrfkey()
 	setconfig("dbversion", myVersion)
 
 	setconfig("servermsg", "<h2>Things happen.</h2>")

          
@@ 164,6 161,13 @@ func initdb() {
 	os.Exit(0)
 }
 
+func setcsrfkey() {
+	var randbytes [16]byte
+	rand.Read(randbytes[:])
+	key := fmt.Sprintf("%x", randbytes)
+	setconfig("csrfkey", key)
+}
+
 func initblobdb(blobdbname string) {
 	_, err := os.Stat(blobdbname)
 	if err == nil {

          
@@ 193,7 197,40 @@ func initblobdb(blobdbname string) {
 }
 
 func adduser() {
-	panic("todo")
+	termvc.Start()
+	defer termvc.Restore()
+	go termvc.Catch(nil)
+
+	db := opendatabase()
+	app := termvc.NewApp()
+	t1 := termvc.NewTextArea()
+	t1.Value = "\n\n\tHello.\n\t\tLet's invite a friend!"
+	var inputs []termvc.Element
+	var offset int
+	namefield := termvc.NewTextInput("username", &offset)
+	inputs = append(inputs, namefield)
+	passfield := termvc.NewPasswordInput("password", &offset)
+	inputs = append(inputs, passfield)
+	btn := termvc.NewButton("let's go!")
+	left := 25
+	inputs = append(inputs, termvc.NewHPad(&left, btn, nil))
+	form := termvc.NewForm(inputs...)
+	t2 := termvc.NewTextArea()
+	group := termvc.NewVStack(t1, form, t2)
+	group.SetFocus(1)
+	app.Element = group
+	app.Screen = termvc.NewScreen()
+	btn.Submit = func() {
+		t2.Value = ""
+		err := createuser(db, namefield.Value, passfield.Value)
+		if err != nil {
+			t2.Value += fmt.Sprintf("error: %s\n", err)
+			return
+		}
+		app.Quit()
+	}
+
+	app.Loop()
 }
 
 func deluser(username string) {

          
M views/honk.html +2 -2
@@ 1,4 1,4 @@ 
-<article class="honk {{ .Honk.Style }}" data-convoy="{{ .Honk.Convoy }}" data-hname="{{ .Honk.Handles }}" data-xid="{{ .Honk.XID }}" data-id="{{ .Honk.ID }}">
+<article id="{{ .Honk.ShortXID }}" class="honk {{ .Honk.Style }}" data-convoy="{{ .Honk.Convoy }}" data-hname="{{ .Honk.Handles }}" data-xid="{{ .Honk.XID }}" data-id="{{ .Honk.ID }}">
 {{ $bonkcsrf := .BonkCSRF }}
 {{ $IsPreview := .IsPreview }}
 {{ $maplink := .MapLink }}

          
@@ 47,7 47,7 @@ in reply to: <a href="{{ .RID }}" rel=no
 {{ end }}
 <br>
 {{ if $bonkcsrf }}
-<span class="left1em clip">convoy: <a class="convoylink" href="/t?c={{ .Convoy }}">{{ .Convoy }}</a></span>
+<span class="left1em clip">convoy: <a class="convoylink" href="/t?c={{ .Convoy }}#{{ .ShortXID }}">{{ .Convoy }}</a></span>
 {{ end }}
 </header>
 <p>

          
M views/honkpage.html +1 -0
@@ 38,3 38,4 @@ 
 </div>
 </div>
 </main>
+<div class="footpad"></div>

          
M views/honkpage.js +39 -5
@@ 194,7 194,7 @@ function statechanger(evt) {
 	}
 	switchtopage(data.name, data.arg)
 }
-function switchtopage(name, arg) {
+function switchtopage(name, arg, anchor) {
 	var stash = curpagestate.name + ":" + curpagestate.arg
 	var honksonpage = document.getElementById("honksonpage")
 	var holder = honksonpage.children[0]

          
@@ 220,6 220,10 @@ function switchtopage(name, arg) {
 		if (msg) {
 			srvel.prepend(msg)
 		}
+		if (anchor) {
+			let el = document.getElementById(anchor)
+			el.scrollIntoView()
+		}
 	} else {
 		// or create one and fill it
 		honksonpage.prepend(document.createElement("div"))

          
@@ 227,6 231,10 @@ function switchtopage(name, arg) {
 		get("/hydra?" + encode(args), function(xhr) {
 			if (xhr.status == 200) {
 				fillinhonks(xhr, false)
+				if (anchor) {
+					let el = document.getElementById(anchor)
+					el.scrollIntoView()
+				}
 			} else {
 				refreshupdate(" status: " + xhr.status)
 			}

          
@@ 246,11 254,14 @@ function pageswitcher(name, arg) {
 		if (name == curpagestate.name && arg == curpagestate.arg) {
 			return false
 		}
-		switchtopage(name, arg)
-		var url = evt.srcElement.href
-		if (!url) {
+		let url = evt.srcElement.href
+		if (!url)
 			url = evt.srcElement.parentElement.href
-		}
+		let anchor
+		let arr = url.split("#")
+		if (arr.length == 2)
+			anchor = arr[1]
+		switchtopage(name, arg, anchor)
 		history.pushState(newpagestate(name, arg), "some title", url)
 		window.scrollTo(0, 0)
 		return false

          
@@ 377,6 388,7 @@ function showhonkform(elem, rid, hname) 
 		ridinput.value = ""
 		honknoise.value = ""
 	}
+	honknoise.ondrop = donkdrop
 	var updateinput = document.getElementById("updatexidinput")
 	updateinput.value = ""
 	var savedfile = document.getElementById("saveddonkxid")

          
@@ 384,6 396,28 @@ function showhonkform(elem, rid, hname) 
 	honknoise.focus()
 	return false
 }
+function donkdrop(evt) {
+	evt.preventDefault()
+	let donks = document.querySelector("#donker input")
+	Array.from(evt.dataTransfer.items).forEach((item) => {
+        if (item.kind == "file") {
+			let olddonks = donks.files
+			let donkarama = new DataTransfer();
+			for (donk of olddonks)
+				donkarama.items.add(donk)
+            let file = item.getAsFile()
+			donkarama.items.add(file)
+			donks.files = donkarama.files;
+            let t = evt.target
+            let start = t.selectionStart
+            let s = t.value.substr(0, start)
+            let e = t.value.substr(start)
+            t.value = s + `<img src=${donks.files.length}>` + e
+            t.selectionStart = start
+            t.selectionEnd = start
+        }
+    })
+}
 function cancelhonking() {
 	hideelement(lehonkform)
 	showelement(lehonkbutton)

          
M views/style.css +4 -0
@@ 448,3 448,7 @@ li.details {
 .fontmonospace {
 	font-family: monospace;
 }
+div.footpad {
+	min-height: 14em;
+	border-top: 1px solid var(--fg-subtle);
+}

          
M web.go +32 -13
@@ 1108,11 1108,18 @@ func thelistingoftheontologies(w http.Re
 		if utf8.RuneCountInString(o.Name) > 24 {
 			continue
 		}
+		if o.Count < 3 {
+			continue
+		}
 		o.Name = o.Name[1:]
 		onts = append(onts, o)
-		if o.Count > 1 {
-			pops = append(pops, o)
-		}
+		pops = append(pops, o)
+	}
+	if len(onts) > 1024 {
+		sort.Slice(onts, func(i, j int) bool {
+			return onts[i].Count > onts[j].Count
+		})
+		onts = onts[:1024]
 	}
 	sort.Slice(onts, func(i, j int) bool {
 		return onts[i].Name < onts[j].Name

          
@@ 1315,6 1322,14 @@ func threadsort(honks []*Honk) []*Honk {
 	thread := make([]*Honk, 0, len(honks))
 	var nextlevel func(p *Honk)
 	level := 0
+	hasreply := func(p *Honk, who string) bool {
+		for _, h := range kids[p.XID] {
+			if h.Honker == who {
+				return true
+			}
+		}
+		return false
+	}
 	nextlevel = func(p *Honk) {
 		levelup := level < 4
 		if pp := honkx[p.RID]; p.RID == "" || (pp != nil && sameperson(p, pp)) {

          
@@ 1330,16 1345,20 @@ func threadsort(honks []*Honk) []*Honk {
 		}
 		p.Style += fmt.Sprintf(" level%d", level)
 		childs := kids[p.XID]
-		if false {
-			sort.SliceStable(childs, func(i, j int) bool {
-				return sameperson(childs[i], p) && !sameperson(childs[j], p)
-			})
-		}
-		if true {
-			sort.SliceStable(childs, func(i, j int) bool {
-				return !sameperson(childs[i], p) && sameperson(childs[j], p)
-			})
-		}
+		sort.SliceStable(childs, func(i, j int) bool {
+			var ipts, jpts int
+			if sameperson(childs[i], p) {
+				ipts += 1
+			} else if hasreply(childs[i], p.Honker) {
+				ipts += 2
+			}
+			if sameperson(childs[j], p) {
+				jpts += 1
+			} else if hasreply(childs[j], p.Honker) {
+				jpts += 2
+			}
+			return ipts > jpts
+		})
 		for _, h := range childs {
 			if !done[h] {
 				done[h] = true