kopia lustrzana https://github.com/nextcloud/social
Merge pull request #666 from StCyr/stCyr_fixHashtags
Identify hashtags from what's found in a toot's DB entrypull/663/head
commit
b871c0d444
|
@ -24,6 +24,7 @@
|
|||
<author mail="jus@bitgrid.net">Julius Härtl</author>
|
||||
<author mail="jonas@violoncello.ch" homepage="https://violoncello.ch">Jonas Sulzer</author>
|
||||
<author mail="hey@jancborchardt.net" homepage="https://jancborchardt.net">Jan-Christoph Borchardt</author>
|
||||
<author mail="cyrpub@bollu.be">Cyrille Bollu</author>
|
||||
<namespace>Social</namespace>
|
||||
<category>social</category>
|
||||
<website>https://github.com/nextcloud/social</website>
|
||||
|
|
|
@ -96,6 +96,7 @@ return [
|
|||
['name' => 'Local#globalActorInfo', 'url' => '/api/v1/global/actor/info', 'verb' => 'GET'],
|
||||
['name' => 'Local#globalActorAvatar', 'url' => '/api/v1/global/actor/avatar', 'verb' => 'GET'],
|
||||
['name' => 'Local#globalAccountsSearch', 'url' => '/api/v1/global/accounts/search', 'verb' => 'GET'],
|
||||
['name' => 'Local#globalTagsSearch', 'url' => '/api/v1/global/tags/search', 'verb' => 'GET'],
|
||||
|
||||
// ['name' => 'Local#documentsCache', 'url' => '/api/v1/documents/cache', 'verb' => 'POST'],
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ use OCA\Social\Service\BoostService;
|
|||
use OCA\Social\Service\CacheActorService;
|
||||
use OCA\Social\Service\DocumentService;
|
||||
use OCA\Social\Service\FollowService;
|
||||
use OCA\Social\Service\HashtagService;
|
||||
use OCA\Social\Service\LikeService;
|
||||
use OCA\Social\Service\MiscService;
|
||||
use OCA\Social\Service\NoteService;
|
||||
|
@ -76,6 +77,9 @@ class LocalController extends Controller {
|
|||
/** @var CacheActorService */
|
||||
private $cacheActorService;
|
||||
|
||||
/** @var HashtagService */
|
||||
private $hashtagService;
|
||||
|
||||
/** @var FollowService */
|
||||
private $followService;
|
||||
|
||||
|
@ -115,6 +119,7 @@ class LocalController extends Controller {
|
|||
* @param string $userId
|
||||
* @param AccountService $accountService
|
||||
* @param CacheActorService $cacheActorService
|
||||
* @param HashtagService $hashtagService
|
||||
* @param FollowService $followService
|
||||
* @param PostService $postService
|
||||
* @param NoteService $noteService
|
||||
|
@ -126,7 +131,8 @@ class LocalController extends Controller {
|
|||
*/
|
||||
public function __construct(
|
||||
IRequest $request, $userId, AccountService $accountService,
|
||||
CacheActorService $cacheActorService, FollowService $followService,
|
||||
CacheActorService $cacheActorService, HashtagService $hashtagService,
|
||||
FollowService $followService,
|
||||
PostService $postService, NoteService $noteService, SearchService $searchService,
|
||||
BoostService $boostService, LikeService $likeService, DocumentService $documentService,
|
||||
MiscService $miscService
|
||||
|
@ -135,6 +141,7 @@ class LocalController extends Controller {
|
|||
|
||||
$this->userId = $userId;
|
||||
$this->cacheActorService = $cacheActorService;
|
||||
$this->hashtagService = $hashtagService;
|
||||
$this->accountService = $accountService;
|
||||
$this->noteService = $noteService;
|
||||
$this->searchService = $searchService;
|
||||
|
@ -777,6 +784,41 @@ class LocalController extends Controller {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @param string $search
|
||||
*
|
||||
* @return DataResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
public function globalTagsSearch(string $search): DataResponse {
|
||||
$this->initViewer();
|
||||
|
||||
if (substr($search, 0, 1) === '#') {
|
||||
$search = substr($search, 1);
|
||||
}
|
||||
|
||||
if ($search === '') {
|
||||
return $this->success(['tags' => [], 'exact' => []]);
|
||||
}
|
||||
|
||||
$match = null;
|
||||
try {
|
||||
$match = $this->hashtagService->getHashtag($search);
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
|
||||
try {
|
||||
$tags = $this->hashtagService->searchHashtags($search, false);
|
||||
|
||||
return $this->success(['tags' => $tags, 'exact' => $match]);
|
||||
} catch (Exception $e) {
|
||||
return $this->fail($e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** // TODO - remove this tag
|
||||
*
|
||||
* @NoCSRFRequired
|
||||
|
|
|
@ -37,7 +37,6 @@ use Doctrine\DBAL\Query\QueryBuilder;
|
|||
use Exception;
|
||||
use OC\DB\SchemaWrapper;
|
||||
use OCA\Social\AP;
|
||||
use OCA\Social\AppInfo\Application;
|
||||
use OCA\Social\Exceptions\DateTimeException;
|
||||
use OCA\Social\Exceptions\InvalidResourceException;
|
||||
use OCA\Social\Model\ActivityPub\Actor\Person;
|
||||
|
@ -307,10 +306,13 @@ class CoreRequestBuilder {
|
|||
*
|
||||
* @param IQueryBuilder $qb
|
||||
* @param string $hashtag
|
||||
* @param bool $all
|
||||
*/
|
||||
protected function searchInHashtag(IQueryBuilder &$qb, string $hashtag) {
|
||||
protected function searchInHashtag(IQueryBuilder &$qb, string $hashtag, bool $all = false) {
|
||||
$dbConn = $this->dbConnection;
|
||||
$this->searchInDBField($qb, 'hashtag', '%' . $dbConn->escapeLikeParameter($hashtag) . '%');
|
||||
$this->searchInDBField(
|
||||
$qb, 'hashtag', (($all) ? '%' : '') . $dbConn->escapeLikeParameter($hashtag) . '%'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -118,12 +118,13 @@ class HashtagsRequest extends HashtagsRequestBuilder {
|
|||
|
||||
/**
|
||||
* @param string $hashtag
|
||||
* @param bool $all
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function searchHashtags(string $hashtag): array {
|
||||
public function searchHashtags(string $hashtag, bool $all): array {
|
||||
$qb = $this->getHashtagsSelectSql();
|
||||
$this->searchInHashtag($qb, $hashtag);
|
||||
$this->searchInHashtag($qb, $hashtag, $all);
|
||||
$this->limitResults($qb, 25);
|
||||
|
||||
$hashtags = [];
|
||||
|
|
|
@ -97,7 +97,7 @@ class StreamRequestBuilder extends CoreRequestBuilder {
|
|||
's.type', 's.to', 's.to_array', 's.cc', 's.bcc', 's.content',
|
||||
's.summary', 's.attachments', 's.published', 's.published_time', 's.cache',
|
||||
's.object_id', 's.attributed_to', 's.in_reply_to', 's.source', 's.local',
|
||||
's.instances', 's.creation', 's.hidden_on_timeline', 's.details'
|
||||
's.instances', 's.creation', 's.hidden_on_timeline', 's.details', 's.hashtags'
|
||||
)
|
||||
->from(self::TABLE_STREAMS, 's');
|
||||
|
||||
|
|
|
@ -36,6 +36,8 @@ use OCA\Social\Db\HashtagsRequest;
|
|||
use OCA\Social\Db\StreamRequest;
|
||||
use OCA\Social\Exceptions\DateTimeException;
|
||||
use OCA\Social\Exceptions\HashtagDoesNotExistException;
|
||||
use OCA\Social\Exceptions\ItemUnknownException;
|
||||
use OCA\Social\Exceptions\SocialAppConfigException;
|
||||
use OCA\Social\Model\ActivityPub\Object\Note;
|
||||
use OCA\Social\Model\ActivityPub\Stream;
|
||||
|
||||
|
@ -100,6 +102,8 @@ class HashtagService {
|
|||
/**
|
||||
* @return int
|
||||
* @throws DateTimeException
|
||||
* @throws ItemUnknownException
|
||||
* @throws SocialAppConfigException
|
||||
*/
|
||||
public function manageHashtags(): int {
|
||||
$current = $this->hashtagsRequest->getAll();
|
||||
|
@ -146,11 +150,12 @@ class HashtagService {
|
|||
|
||||
/**
|
||||
* @param string $hashtag
|
||||
* @param bool $all
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function searchHashtags(string $hashtag): array {
|
||||
return $this->hashtagsRequest->searchHashtags($hashtag);
|
||||
public function searchHashtags(string $hashtag, bool $all = false): array {
|
||||
return $this->hashtagsRequest->searchHashtags($hashtag, $all);
|
||||
}
|
||||
|
||||
|
||||
|
@ -159,6 +164,8 @@ class HashtagService {
|
|||
*
|
||||
* @return Stream[]
|
||||
* @throws DateTimeException
|
||||
* @throws ItemUnknownException
|
||||
* @throws SocialAppConfigException
|
||||
*/
|
||||
private function getTrendSince(int $timestamp): array {
|
||||
$result = [];
|
||||
|
|
|
@ -139,7 +139,7 @@ class SearchService {
|
|||
}
|
||||
|
||||
try {
|
||||
$hashtags = $this->hashtagService->searchHashtags($search);
|
||||
$hashtags = $this->hashtagService->searchHashtags($search, true);
|
||||
$result['result'] = $hashtags;
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -28,7 +28,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"vue-masonry-css": "^1.0.3",
|
||||
"hashtag-regex": "^2.0.0",
|
||||
"linkifyjs": "^2.1.8",
|
||||
"nextcloud-axios": "^0.2.0",
|
||||
"nextcloud-vue": "^0.11.4",
|
||||
|
|
|
@ -328,6 +328,9 @@
|
|||
width: 16px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
.hashtag {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
|
||||
|
@ -365,47 +368,97 @@ export default {
|
|||
search: '',
|
||||
replyTo: null,
|
||||
tributeOptions: {
|
||||
lookup: function(item) {
|
||||
return item.key + item.value
|
||||
},
|
||||
menuItemTemplate: function(item) {
|
||||
return '<img src="' + item.original.avatar + '" /><div>'
|
||||
+ '<span class="displayName">' + item.original.key + '</span>'
|
||||
+ '<span class="account">' + item.original.value + '</span>'
|
||||
+ '</div>'
|
||||
},
|
||||
selectTemplate: function(item) {
|
||||
return '<span class="mention" contenteditable="false">'
|
||||
+ '<a href="' + item.original.url + '" target="_blank"><img src="' + item.original.avatar + '" />@' + item.original.value + '</a></span>'
|
||||
},
|
||||
values: (text, cb) => {
|
||||
let users = []
|
||||
collection: [
|
||||
{
|
||||
trigger: '@',
|
||||
lookup: function(item) {
|
||||
return item.key + item.value
|
||||
},
|
||||
menuItemTemplate: function(item) {
|
||||
return '<img src="' + item.original.avatar + '" /><div>'
|
||||
+ '<span class="displayName">' + item.original.key + '</span>'
|
||||
+ '<span class="account">' + item.original.value + '</span>'
|
||||
+ '</div>'
|
||||
},
|
||||
selectTemplate: function(item) {
|
||||
return '<span class="mention" contenteditable="false">'
|
||||
+ '<a href="' + item.original.url + '" target="_blank"><img src="' + item.original.avatar + '" />@' + item.original.value + '</a></span>'
|
||||
},
|
||||
values: (text, cb) => {
|
||||
let users = []
|
||||
|
||||
if (text.length < 1) {
|
||||
cb(users)
|
||||
if (text.length < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
this.remoteSearchAccounts(text).then((result) => {
|
||||
if (result.data.result.exact) {
|
||||
let user = result.data.result.exact
|
||||
users.push({
|
||||
key: user.preferredUsername,
|
||||
value: user.account,
|
||||
url: user.url,
|
||||
avatar: user.local ? OC.generateUrl(`/avatar/${user.preferredUsername}/32`) : ''// TODO: use real avatar from server
|
||||
})
|
||||
}
|
||||
for (var i in result.data.result.accounts) {
|
||||
let user = result.data.result.accounts[i]
|
||||
users.push({
|
||||
key: user.preferredUsername,
|
||||
value: user.account,
|
||||
url: user.url,
|
||||
avatar: user.local ? OC.generateUrl(`/avatar/${user.preferredUsername}/32`) : OC.generateUrl(`apps/social/api/v1/global/actor/avatar?id=${user.id}`)
|
||||
})
|
||||
}
|
||||
if (users.length > 0) {
|
||||
cb(users)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
trigger: '#',
|
||||
menuItemTemplate: function(item) {
|
||||
return item.original.value
|
||||
},
|
||||
selectTemplate: function(item) {
|
||||
let tag = ''
|
||||
// item is undefined if selectTemplate is called from a noMatchTemplate menu
|
||||
if (typeof item === 'undefined') {
|
||||
tag = this.currentMentionTextSnapshot
|
||||
} else {
|
||||
tag = item.original.value
|
||||
}
|
||||
return '<span class="hashtag" contenteditable="false">'
|
||||
+ '<a href="' + OC.generateUrl('/timeline/tags/' + tag) + '" target="_blank">#' + tag + '</a></span>'
|
||||
},
|
||||
values: (text, cb) => {
|
||||
let tags = []
|
||||
|
||||
if (text.length < 1) {
|
||||
return
|
||||
}
|
||||
this.remoteSearchHashtags(text).then((result) => {
|
||||
if (result.data.result.exact) {
|
||||
tags.push({
|
||||
key: result.data.result.exact,
|
||||
value: result.data.result.exact
|
||||
})
|
||||
}
|
||||
for (var i in result.data.result.tags) {
|
||||
let tag = result.data.result.tags[i]
|
||||
tags.push({
|
||||
key: tag.hashtag,
|
||||
value: tag.hashtag
|
||||
})
|
||||
}
|
||||
if (tags.length > 0) {
|
||||
cb(tags)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
this.remoteSearch(text).then((result) => {
|
||||
if (result.data.result.exact) {
|
||||
let user = result.data.result.exact
|
||||
users.push({
|
||||
key: user.preferredUsername,
|
||||
value: user.account,
|
||||
url: user.url,
|
||||
avatar: user.local ? OC.generateUrl(`/avatar/${user.preferredUsername}/32`) : ''// TODO: use real avatar from server
|
||||
})
|
||||
}
|
||||
for (var i in result.data.result.accounts) {
|
||||
let user = result.data.result.accounts[i]
|
||||
users.push({
|
||||
key: user.preferredUsername,
|
||||
value: user.account,
|
||||
url: user.url,
|
||||
avatar: user.local ? OC.generateUrl(`/avatar/${user.preferredUsername}/32`) : OC.generateUrl(`apps/social/api/v1/global/actor/avatar?id=${user.id}`)
|
||||
})
|
||||
}
|
||||
cb(users)
|
||||
})
|
||||
}
|
||||
]
|
||||
},
|
||||
menuOpened: false
|
||||
|
||||
|
@ -528,29 +581,29 @@ export default {
|
|||
var em = document.createTextNode(emoji.getAttribute('alt'))
|
||||
emoji.replaceWith(em)
|
||||
})
|
||||
let content = element.innerText.trim()
|
||||
let contentHtml = element.innerHTML
|
||||
let to = []
|
||||
let hashtags = []
|
||||
const mentionRegex = /@(([\w-_.]+)(@[\w-.]+)?)/g
|
||||
let match = null
|
||||
do {
|
||||
match = mentionRegex.exec(content)
|
||||
match = mentionRegex.exec(contentHtml)
|
||||
if (match) {
|
||||
to.push(match[1])
|
||||
}
|
||||
} while (match)
|
||||
|
||||
const hashtagRegex = /#([\w-_.]+)/g
|
||||
const hashtagRegex = />#([^<]+)</g
|
||||
match = null
|
||||
do {
|
||||
match = hashtagRegex.exec(content)
|
||||
match = hashtagRegex.exec(contentHtml)
|
||||
if (match) {
|
||||
hashtags.push(match[1])
|
||||
}
|
||||
} while (match)
|
||||
|
||||
let data = {
|
||||
content: content,
|
||||
content: element.innerText.trim(),
|
||||
to: to,
|
||||
hashtags: hashtags,
|
||||
type: this.type
|
||||
|
@ -575,8 +628,11 @@ export default {
|
|||
this.$store.dispatch('refreshTimeline')
|
||||
})
|
||||
},
|
||||
remoteSearch(text) {
|
||||
remoteSearchAccounts(text) {
|
||||
return axios.get(OC.generateUrl('apps/social/api/v1/global/accounts/search?search=' + text))
|
||||
},
|
||||
remoteSearchHashtags(text) {
|
||||
return axios.get(OC.generateUrl('apps/social/api/v1/global/tags/search?search=' + text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,8 +60,6 @@ import PostAttachment from './PostAttachment'
|
|||
|
||||
pluginMention(linkify)
|
||||
|
||||
const hashtagRegex = require('hashtag-regex')
|
||||
|
||||
export default {
|
||||
name: 'TimelinePost',
|
||||
components: {
|
||||
|
@ -102,7 +100,7 @@ export default {
|
|||
return Date.parse(this.item.published)
|
||||
},
|
||||
formatedMessage() {
|
||||
var message = this.item.content
|
||||
let message = this.item.content
|
||||
if (typeof message === 'undefined') {
|
||||
return ''
|
||||
}
|
||||
|
@ -114,7 +112,9 @@ export default {
|
|||
}
|
||||
}
|
||||
})
|
||||
message = this.mangleHashtags(message)
|
||||
if (this.item.hashtags !== undefined) {
|
||||
message = this.mangleHashtags(message)
|
||||
}
|
||||
message = this.$twemoji.parse(message)
|
||||
return message
|
||||
},
|
||||
|
@ -140,10 +140,12 @@ export default {
|
|||
methods: {
|
||||
mangleHashtags(msg) {
|
||||
// Replace hashtag's href parameter with local ones
|
||||
const regex = hashtagRegex()
|
||||
msg = msg.replace(regex, function(matched) {
|
||||
var a = '<a href="' + OC.generateUrl('/apps/social/timeline/tags/' + matched.substring(1)) + '">' + matched + '</a>'
|
||||
return a
|
||||
this.item.hashtags.forEach(tag => {
|
||||
let patt = new RegExp('#' + tag, 'gi')
|
||||
msg = msg.replace(patt, function(matched) {
|
||||
var a = '<a href="' + OC.generateUrl('/apps/social/timeline/tags/' + matched.substring(1)) + '">' + matched + '</a>'
|
||||
return a
|
||||
})
|
||||
})
|
||||
return msg
|
||||
},
|
||||
|
|
Ładowanie…
Reference in New Issue