123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604 |
- <template>
- <div class="container">
- <div v-if="loading" class="pt-5 text-center">
- <div class="spinner-border" role="status">
- <span class="sr-only">Loading…</span>
- </div>
- </div>
- <div v-if="networkError" class="pt-5 text-center">
- <p class="lead font-weight-lighter">An error occured, results could not be loaded.<br> Please try again later.</p>
- </div>
- <div v-if="!loading && !networkError" class="mt-5">
- <div v-if="analysis == 'all'" class="row">
- <div class="col-12 d-flex justify-content-between align-items-center">
- <p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
- <div v-if="placesSearchEnabled" title="Show Places" data-toggle="tooltip">
- <span v-if="results.placesPagination.total > 0" class="badge badge-light mr-2 p-1 border" style="margin-top:-5px;">{{formatCount(results.placesPagination.total)}}</span>
- <div class="d-inline custom-control custom-switch">
- <input type="checkbox" class="custom-control-input" id="placesSwitch" v-model="showPlaces">
- <label class="custom-control-label font-weight-bold text-sm text-lighter" for="placesSwitch"><i class="fas fa-map-marker-alt"></i></label>
- </div>
- </div>
- </div>
- <div class="col-12 mb-5">
- <hr>
- </div>
- <div v-if="placesSearchEnabled && showPlaces" class="col-12">
- <div class="mb-4">
- <p class="text-secondary small font-weight-bold">PLACES <span class="pl-1 text-lighter">({{results.placesPagination.total}})</span></p>
- </div>
- <div v-if="results.places.length" class="mb-5">
- <a v-for="(hashtag, index) in results.places" class="mr-3 pr-4 d-inline-block text-decoration-none" :href="buildUrl('places', hashtag)">
- <div class="pb-2">
- <div class="media align-items-center py-2">
- <div class="media-body text-truncate">
- <p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
- <i class="fas fa-map-marker-alt text-lighter mr-2"></i> {{hashtag.value}}
- </p>
- </div>
- </div>
- </div>
- </a>
- <p v-if="results.places.length == 20 || placesCursor > 0" class="text-center mt-3">
- <a v-if="placesCursor == 1" href="#" class="btn btn-outline-secondary btn-sm font-weight-bold py-0 disabled" disabled>
- <i class="fas fa-chevron-left mr-2"></i> Previous
- </a>
- <a v-else href="#" @click.prevent="placesPrevPage()" class="btn btn-outline-secondary btn-sm font-weight-bold py-0">
- <i class="fas fa-chevron-left mr-2"></i> Previous
- </a>
- <span class="mx-4 small text-lighter">{{placesCursor}}/{{results.placesPagination.last_page}}</span>
- <a v-if="placesCursor !== results.placesPagination.last_page" @click.prevent="placesNextPage()" href="#" class="btn btn-primary btn-sm font-weight-bold py-0">
- Next <i class="fas fa-chevron-right ml-2"></i>
- </a>
- <a v-else href="#" class="btn btn-primary btn-sm font-weight-bold py-0 disabled" disabled>
- Next <i class="fas fa-chevron-right ml-2"></i>
- </a>
- </p>
- </div>
- <div v-else>
- <div class="border py-3 text-center font-weight-bold">No results found</div>
- </div>
- </div>
- <div class="col-md-3">
- <div class="mb-4">
- <p class="text-secondary small font-weight-bold">HASHTAGS <span class="pl-1 text-lighter">({{results.hashtags.length}})</span></p>
- </div>
- <div v-if="results.hashtags.length">
- <a v-for="(hashtag, index) in results.hashtags" class="mb-2 result-card" :href="buildUrl('hashtag', hashtag)">
- <div class="pb-3">
- <div class="media align-items-center py-2 pr-3">
- <span class="d-inline-flex align-items-center justify-content-center border rounded-circle mr-3" style="width: 50px;height: 50px;">
- <i class="fas fa-hashtag text-muted"></i>
- </span>
- <div class="media-body text-truncate">
- <p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
- #{{hashtag.value}}
- </p>
- <p v-if="hashtag.count > 2" class="mb-0 small font-weight-bold text-muted text-uppercase">
- {{hashtag.count}} posts
- </p>
- </div>
- </div>
- </div>
- </a>
- </div>
- <div v-else>
- <div class="border py-3 text-center font-weight-bold">No results found</div>
- </div>
- </div>
- <div class="col-md-5">
- <div class="mb-4">
- <p class="text-secondary small font-weight-bold">PROFILES <span class="pl-1 text-lighter">({{results.profiles.length}})</span></p>
- </div>
- <div v-if="results.profiles.length">
- <a v-for="(profile, index) in results.profiles" class="mb-2 result-card" :href="buildUrl('profile', profile)">
- <div class="pb-3">
- <div class="media align-items-center py-2 pr-3">
- <img class="mr-3 rounded-circle border" :src="profile.avatar" width="50px" height="50px">
- <div class="media-body">
- <p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="profile.value">
- {{profile.value}}
- </p>
- <p class="mb-0 small font-weight-bold text-muted text-uppercase">
- {{profile.entity.post_count}} Posts
- </p>
- </div>
- <div class="ml-3">
- <a v-if="profile.entity.following" class="btn btn-primary btn-sm font-weight-bold text-uppercase py-0" :href="buildUrl('profile', profile)">Following</a>
- <a v-else class="btn btn-outline-primary btn-sm font-weight-bold text-uppercase py-0" :href="buildUrl('profile', profile)">View</a>
- </div>
- </div>
- </div>
- </a>
- </div>
- <div v-else>
- <div class="border py-3 text-center font-weight-bold">No results found</div>
- </div>
- </div>
- <div class="col-md-4">
- <div class="mb-4">
- <p class="text-secondary small font-weight-bold">STATUSES <span class="pl-1 text-lighter">({{results.statuses.length}})</span></p>
- </div>
- <div v-if="results.statuses.length">
- <a v-for="(status, index) in results.statuses" class="mr-2 result-card" :href="buildUrl('status', status)">
- <img :src="status.thumb" width="90px" height="90px" class="mb-2">
- </a>
- </div>
- <div v-else>
- <div class="border py-3 text-center font-weight-bold">No results found</div>
- </div>
- </div>
- </div>
- <div v-else-if="analysis == 'hashtag'" class="row">
- <div class="col-12 mb-5">
- <p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
- <hr>
- </div>
- <div class="col-md-6 offset-md-3">
- <div class="mb-4">
- <p class="text-secondary small font-weight-bold">HASHTAGS <span class="pl-1 text-lighter">({{results.hashtags.length}})</span></p>
- </div>
- <div v-if="results.hashtags.length">
- <a v-for="(hashtag, index) in results.hashtags" class="mb-2 result-card" :href="buildUrl('hashtag', hashtag)">
- <div class="pb-3">
- <div class="media align-items-center py-2 pr-3">
- <span class="d-inline-flex align-items-center justify-content-center border rounded-circle mr-3" style="width: 50px;height: 50px;">
- <i class="fas fa-hashtag text-muted"></i>
- </span>
- <div class="media-body">
- <p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
- #{{hashtag.value}}
- </p>
- <p v-if="hashtag.count > 2" class="mb-0 small font-weight-bold text-muted text-uppercase">
- {{hashtag.count}} posts
- </p>
- </div>
- </div>
- </div>
- </a>
- </div>
- <div v-else>
- <div class="border py-3 text-center font-weight-bold">No results found</div>
- </div>
- </div>
- </div>
- <div v-else-if="analysis == 'profile'" class="row">
- <div class="col-12 mb-5">
- <p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
- <hr>
- </div>
- <div class="col-md-6 offset-md-3">
- <div class="mb-4">
- <p class="text-secondary small font-weight-bold">PROFILES <span class="pl-1 text-lighter">({{results.profiles.length}})</span></p>
- </div>
- <div v-if="results.profiles.length">
- <div v-for="(profile, index) in results.profiles" class="card mb-4">
- <div class="card-header p-0 m-0">
- <div style="width: 100%;height: 140px;background: #0070b7"></div>
- </div>
- <div class="card-body">
- <div class="text-center mt-n5 mb-4">
- <img class="rounded-circle p-1 border mt-n4 bg-white shadow" :src="profile.entity.thumb" width="90px" height="90px;" onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
- </div>
- <p class="text-center lead font-weight-bold mb-1">{{profile.value}}</p>
- <p class="text-center text-muted small text-uppercase mb-4"><!-- 2 followers --></p>
- <div class="d-flex justify-content-center">
- <button v-if="profile.entity.following" type="button" class="btn btn-outline-secondary btn-sm py-1 px-4 text-uppercase font-weight-bold mr-3" style="font-weight: 500">Following</button>
- <a class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold" :href="buildUrl('profile',profile)" style="font-weight: 500">View Profile</a>
- </div>
- </div>
- </div>
- </div>
- <div v-else>
- <div class="border py-3 text-center font-weight-bold">No results found</div>
- </div>
- </div>
- </div>
- <div v-else-if="analysis == 'webfinger'" class="row">
- <div class="col-12 mb-5">
- <p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
- <hr>
- <div class="col-md-6 offset-md-3">
- <div v-for="(profile, index) in results.profiles" class="card mb-2">
- <div class="card-header p-0 m-0">
- <div style="width: 100%;height: 140px;background: #0070b7"></div>
- </div>
- <div class="card-body">
- <div class="text-center mt-n5 mb-4">
- <img class="rounded-circle p-1 border mt-n4 bg-white shadow" :src="profile.entity.thumb" width="90px" height="90px;" onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
- </div>
- <p class="text-center lead font-weight-bold mb-1">{{profile.value}}</p>
- <p class="text-center text-muted small text-uppercase mb-4"><!-- 2 followers --></p>
- <div class="d-flex justify-content-center">
- <!-- <button v-if="profile.entity.following" type="button" class="btn btn-outline-secondary btn-sm py-1 px-4 text-uppercase font-weight-bold mr-3" style="font-weight: 500">Unfollow</button> -->
- <!-- <button v-else type="button" class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold mr-3" style="font-weight: 500">Follow</button> -->
- <a class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold" :href="'/i/web/profile/_/' + profile.entity.id" style="font-weight: 500">View Profile</a>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div v-else-if="analysis == 'remote'" class="row">
- <div class="col-12 mb-5">
- <p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
- <hr>
- </div>
- <div v-if="results.profiles.length" class="col-md-6 offset-3">
- <a v-for="(profile, index) in results.profiles" class="mb-2 result-card" :href="buildUrl('profile', profile)">
- <div class="pb-3">
- <div class="media align-items-center py-2 pr-3">
- <img class="mr-3 rounded-circle border" :src="profile.entity.thumb" width="50px" height="50px" onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
- <div class="media-body">
- <p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="profile.value">
- {{profile.value}}
- </p>
- <p class="mb-0 small font-weight-bold text-muted text-uppercase">
- {{profile.entity.post_count}} Posts
- </p>
- </div>
- <div class="ml-3">
- <a v-if="profile.entity.following" class="btn btn-primary btn-sm font-weight-bold text-uppercase py-0" :href="buildUrl('profile', profile)">Following</a>
- <a v-else class="btn btn-outline-primary btn-sm font-weight-bold text-uppercase py-0" :href="buildUrl('profile', profile)">View</a>
- </div>
- </div>
- </div>
- </a>
- </div>
- <div v-if="results.statuses.length" class="col-md-6 offset-3">
- <a v-for="(status, index) in results.statuses" class="mr-2 result-card" :href="buildUrl('status', status)">
- <img :src="status.thumb" width="90px" height="90px" class="mb-2" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
- </a>
- </div>
- </div>
- <div v-else-if="analysis == 'remotePost'" class="row">
- <div class="col-12 mb-5">
- <p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
- <hr>
- </div>
- <div class="col-md-6 offset-md-3">
- <div v-if="results.statuses.length">
- <div v-for="(status, index) in results.statuses" class="card mb-4 shadow-none border">
- <div class="card-header p-0 m-0">
- <div style="width: 100%;height: 200px;background: #fff">
- <div class="pt-4 text-center">
- <img :src="status.thumb" class="img-fluid border" style="max-height: 140px;">
- </div>
- </div>
- </div>
- <div class="card-body">
- <div class="mt-n4 mb-2">
- <div class="media">
-
- <img class="rounded-circle p-1 mr-2 border mt-n3 bg-white shadow" src="/storage/avatars/default.png" width="70px" height="70px;" onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
- <div class="media-body pt-3">
- <p class="font-weight-bold mb-0">{{status.username}}</p>
- </div>
- <div class="float-right pt-3">
- <p class="small mb-0 text-muted">{{status.timestamp}}</p>
- </div>
- </div>
- </div>
- <p class="text-center mb-3 lead" v-html="status.caption"></p>
- <!-- <p class="text-center text-muted small text-uppercase mb-4">2 likes</p> -->
- <!-- <div class="d-flex justify-content-center">
- <a class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold" :href="status.url" style="font-weight: 500">View Post</a>
- </div> -->
- </div>
- <div class="card-footer">
- <a class="btn btn-primary btn-block font-weight-bold rounded-0" :href="status.url">View Post</a>
- </div>
- </div>
- </div>
- <div v-else>
- <div class="border py-3 text-center font-weight-bold">No results found</div>
- </div>
- </div>
- </div>
- <div v-else class="col-12">
- <p class="text-center text-muted lead font-weight-bold">No results found</p>
- </div>
- </div>
- </div>
- </template>
- <style type="text/css" scoped>
- .result-card {
- text-decoration: none;
- }
- .result-card .media:hover {
- background: #EDF2F7;
- }
- @media (min-width: 1200px) {
- .container {
- max-width: 995px;
- }
- }
- </style>
- <script type="text/javascript">
- export default {
- props: ['query', 'profileId'],
- data() {
- return {
- loading: true,
- networkError: false,
- results: {
- hashtags: [],
- profiles: [],
- statuses: [],
- places: [],
- },
- filters: {
- hashtags: true,
- profiles: true,
- statuses: true
- },
- analysis: 'profile',
- showPlaces: false,
- placesCursor: 1,
- placesCache: [],
- placesSearchEnabled: false,
- searchVersion: 2
- }
- },
- beforeMount() {
- this.bootSearch();
- },
- mounted() {
- $('.search-bar input').val(this.query);
- },
- updated() {
- $('[data-toggle="tooltip"]').tooltip();
- },
- methods: {
- bootSearch() {
- let lexer = this.searchLexer();
- this.analysis = lexer;
- this.fetchSearchResults();
- },
- fetchSearchResults() {
- if(this.analysis == 'remote') {
- let term = this.query;
- let parsed = new URL(term);
- if(parsed.host === window.location.host) {
- window.location.href = term;
- return;
- }
- }
- this.searchContext(this.analysis);
- },
- followProfile(profile, index) {
- this.loading = true;
- axios.post('/i/follow', {
- item: profile.entity.id
- }).then(res => {
- if(profile.entity.local == true) {
- this.fetchSearchResults();
- return;
- } else {
- this.loading = false;
- this.results.profiles[index].entity.follow_request = true;
- return;
- }
- }).catch(err => {
- if(err.response.data.message) {
- swal('Error', err.response.data.message, 'error');
- }
- });
- },
- searchLexer() {
- let q = this.query;
- if(q.startsWith('#')) {
- return 'hashtag';
- }
- if((q.match(/@/g) || []).length == 2) {
- return 'webfinger';
- }
- if(q.startsWith('@')) {
- return 'profile';
- }
- if(q.startsWith('https://')) {
- return 'remote';
- }
- return 'all';
- },
- buildUrl(type = 'hashtag', obj) {
- switch(type) {
- case 'hashtag':
- return obj.url + '?src=search';
- break;
- case 'profile':
- if(obj.entity.local == true) {
- return obj.url;
- }
- return '/i/web/profile/_/' + obj.entity.id;
- break;
- default:
- return obj.url + '?src=search';
- break;
- }
- },
- searchContext(type) {
- switch(type) {
- case 'all':
- axios.get('/api/search', {
- params: {
- 'q': this.query,
- 'src': 'metro',
- 'v': this.searchVersion,
- 'scope': 'all'
- }
- }).then(res => {
- let results = res.data;
- this.results.hashtags = results.hashtags ? results.hashtags : [];
- this.results.profiles = results.profiles ? results.profiles : [];
- this.results.statuses = results.posts ? results.posts : [];
- this.results.places = results.places ? results.places : [];
- this.placesCache = results.places;
- this.results.placesPagination = results.placesPagination ? results.placesPagination : [];
- this.loading = false;
- }).catch(err => {
- this.loading = false;
- this.networkError = true;
- });
- break;
- case 'remote':
- axios.get('/api/search', {
- params: {
- 'q': this.query,
- 'src': 'metro',
- 'v': this.searchVersion,
- 'scope': 'remote'
- }
- }).then(res => {
- let results = res.data;
- this.results.hashtags = results.hashtags ? results.hashtags : [];
- this.results.profiles = results.profiles ? results.profiles : [];
- this.results.statuses = results.posts ? results.posts : [];
- if(this.results.profiles.length) {
- this.analysis = 'profile';
- }
- if(this.results.statuses.length) {
- this.analysis = 'remotePost';
- }
- this.loading = false;
- }).catch(err => {
- this.loading = false;
- this.networkError = true;
- });
- break;
- case 'hashtag':
- axios.get('/api/search', {
- params: {
- 'q': this.query.slice(1),
- 'src': 'metro',
- 'v': this.searchVersion,
- 'scope': 'hashtag'
- }
- }).then(res => {
- let results = res.data;
- this.results.hashtags = results.hashtags ? results.hashtags : [];
- this.results.profiles = results.profiles ? results.profiles : [];
- this.results.statuses = results.posts ? results.posts : [];
- this.loading = false;
- }).catch(err => {
- this.loading = false;
- this.networkError = true;
- });
- break;
- case 'profile':
- axios.get('/api/search', {
- params: {
- 'q': this.query,
- 'src': 'metro',
- 'v': this.searchVersion,
- 'scope': 'profile'
- }
- }).then(res => {
- let results = res.data;
- this.results.hashtags = results.hashtags ? results.hashtags : [];
- this.results.profiles = results.profiles ? results.profiles : [];
- this.results.statuses = results.posts ? results.posts : [];
- this.loading = false;
- }).catch(err => {
- this.loading = false;
- this.networkError = true;
- });
- break;
- case 'webfinger':
- axios.get('/api/search', {
- params: {
- 'q': this.query,
- 'src': 'metro',
- 'v': this.searchVersion,
- 'scope': 'webfinger'
- }
- }).then(res => {
- let results = res.data;
- this.results.hashtags = [];
- this.results.profiles = results.profiles;
- this.results.statuses = [];
- this.loading = false;
- }).catch(err => {
- this.loading = false;
- this.networkError = true;
- });
- break;
- default:
- this.loading = false;
- this.networkError = true;
- break;
- }
- },
- placesPrevPage() {
- this.placesCursor--;
- if(this.placesCursor == 1) {
- this.results.places = this.placesCache.slice(0, 20);
- return;
- }
- let plc = this.placesCursor * 20;
- this.results.places = this.placesCache.slice(plc, 20);
- return;
- },
- placesNextPage() {
- this.placesCursor++;
- let plc = this.placesCursor * 20;
- if(this.placesCache.length > 20) {
- this.results.places = this.placesCache.slice(this.placesCursor == 1 ? 0 : plc, 20);
- return;
- }
- axios.get('/api/search', {
- params: {
- 'q': this.query,
- 'src': 'metro',
- 'v': this.searchVersion,
- 'scope': 'all',
- 'page': this.placesCursor
- }
- }).then(res => {
- let results = res.data;
- this.results.places = results.places ? results.places : [];
- this.placesCache.push(...results.places);
- this.loading = false;
- }).catch(err => {
- this.loading = false;
- this.networkError = true;
- });
- },
- formatCount(num) {
- let count = window.App.util.format.count(num);
- return count;
- }
- }
- }
- </script>
|