Merge pull request #666 from StCyr/stCyr_fixHashtags

Identify hashtags from what's found in a toot's DB entry
pull/663/head
Maxence Lange 2019-08-02 13:00:22 -01:00 zatwierdzone przez GitHub
commit b871c0d444
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
12 zmienionych plików z 3174 dodań i 3068 usunięć

Wyświetl plik

@ -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>

Wyświetl plik

@ -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'],

Wyświetl plik

@ -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

Wyświetl plik

@ -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) . '%'
);
}

Wyświetl plik

@ -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 = [];

Wyświetl plik

@ -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');

Wyświetl plik

@ -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 = [];

Wyświetl plik

@ -139,7 +139,7 @@ class SearchService {
}
try {
$hashtags = $this->hashtagService->searchHashtags($search);
$hashtags = $this->hashtagService->searchHashtags($search, true);
$result['result'] = $hashtags;
} catch (Exception $e) {
}

6003
package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -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",

Wyświetl plik

@ -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))
}
}
}

Wyświetl plik

@ -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
},