Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ ui-debug.log
# Firebase client config — every developer supplies their own.
# The committed template is app/google-services.json.example.
/app/google-services.json

# Local Claude Code settings — per-machine tool permissions, not project config.
/.claude
172 changes: 155 additions & 17 deletions README.md

Large diffs are not rendered by default.

82 changes: 0 additions & 82 deletions app/README.md

This file was deleted.

1 change: 0 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.ksp)
alias(libs.plugins.google.services)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ package duygu.yilmaz.campusnote.data.model
data class LeaderboardEntry(
val docId: String,
val title: String,
val uploaderEmail: String,
val uploaderName: String,
val department: String,
val ratingCount: Long,
val ratingSum: Long = 0L
) {
/** Notu yükleyen kişinin gösterilecek adı; tam e-posta arayüze çıkmaz. */
val uploaderName: String get() = uploaderEmail.uploaderName()
}
)
8 changes: 2 additions & 6 deletions app/src/main/java/duygu/yilmaz/campusnote/data/model/Post.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,15 @@ data class Post(
val id: String,
val title: String,
val desc: String,
val authorEmail: String,
val uploaderName: String,
val department: String,
val timeMills: Long,
val uploaderUid: String = "",
val avgRating: Double = 0.0,
val ratingCount: Long = 0L,
val ratingSum: Long = 0L,
val course: String = "",
val tag: String = "",
val fileName: String = "",
val fileType: String = "",
val fileSize: Long = 0L
) {
/** Notu yükleyen kişinin gösterilecek adı; tam e-posta arayüze çıkmaz. */
val uploaderName: String get() = authorEmail.uploaderName()
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package duygu.yilmaz.campusnote.data.model
data class RatingTotals(
val sum: Long,
val count: Long,
val average: Double,
/** Kullanıcı ilk kez mi oy verdi, yoksa eski oyunu mu değiştirdi. */
val updatedExistingRating: Boolean
)
Expand Down Expand Up @@ -53,12 +52,10 @@ object RatingCalculator {

// Toplam hiçbir koşulda negatife düşmemeli; eski veriler bozuksa taban 0.
val sum = (currentSum + delta).coerceAtLeast(0L)
val average = if (count == 0L) 0.0 else sum.toDouble() / count

return RatingTotals(
sum = sum,
count = count,
average = average,
updatedExistingRating = updatedExistingRating
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package duygu.yilmaz.campusnote.data.model

data class RatingResult(
val average: Double,
val count: Long,
val sum: Long,
val updatedExistingRating: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import duygu.yilmaz.campusnote.data.model.LeaderboardEntry
import duygu.yilmaz.campusnote.data.model.NoteDraft
import duygu.yilmaz.campusnote.data.model.NoteUpdate
import duygu.yilmaz.campusnote.data.model.Post
import duygu.yilmaz.campusnote.data.model.uploaderName
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
Expand All @@ -26,7 +27,7 @@ class FirebaseNoteRepository(
override suspend fun createNote(
draft: NoteDraft,
uploaderUid: String,
uploaderEmail: String,
uploaderName: String,
department: String
) {
val noteReference = firestore.collection(NOTES_COLLECTION).document()
Expand All @@ -37,11 +38,10 @@ class FirebaseNoteRepository(
"tag" to draft.tag,
"department" to department,
"uploaderUid" to uploaderUid,
"uploaderEmail" to uploaderEmail,
"uploaderName" to uploaderName,
"createdAt" to FieldValue.serverTimestamp(),
"ratingSum" to 0L,
"ratingCount" to 0L,
"avgRating" to 0.0,
"fileName" to draft.fileName,
"fileType" to draft.fileType,
"fileSize" to draft.fileSize
Expand Down Expand Up @@ -93,12 +93,12 @@ class FirebaseNoteRepository(

override fun observeNotesByUploader(
uploaderUid: String,
defaultUploaderEmail: String
defaultUploaderName: String
): Flow<List<Post>> =
observePosts(
query = firestore.collection(NOTES_COLLECTION)
.whereEqualTo(UPLOADER_UID_FIELD, uploaderUid),
defaultUploaderEmail = defaultUploaderEmail
defaultUploaderName = defaultUploaderName
)

/**
Expand Down Expand Up @@ -135,10 +135,34 @@ class FirebaseNoteRepository(
}

/** Notu ve içerik dokümanını birlikte siler — Firestore alt koleksiyonları kendiliğinden silmez. */
/**
* Notu, dosyasını ve ona verilmiş oyları tek commit'te siler.
*
* Oylar eskiden kalıyordu: `ratings` ayrı bir üst düzey koleksiyon, notun alt
* koleksiyonu değil, dolayısıyla notu silmek onlara dokunmuyordu. Geride kalan
* doküman kimseye görünmüyor ama sonsuza kadar duruyor ve `<uid>_<noteId>`
* kimliğini işgal ediyor.
*
* Aynı batch'te olmaları şart, süslemeden değil: güvenlik kuralı bir oyun
* silinmesine ancak notu da aynı commit'te siliniyorsa izin veriyor
* (`existsAfter`). Ayrı ayrı gönderilseler ikisi de reddedilirdi — ve reddin
* kendisi doğru, çünkü tek başına silinen bir oy, oy verenin ikinci kez
* oylayıp sayıyı şişirmesine kapı açardı.
*
* Sınır: Firestore bir batch'te en çok 500 yazma kabul ediyor, yani ~498'den
* fazla oy almış bir not bu yolla silinemez. Bölüm ölçeğinde ulaşılacak bir
* sayı değil; aşılırsa silme hata verir, sessizce yarım kalmaz.
*/
override suspend fun deleteNote(noteId: String) {
val noteReference = firestore.collection(NOTES_COLLECTION).document(noteId)

val ratings = firestore.collection(RATINGS_COLLECTION)
.whereEqualTo(NOTE_ID_FIELD, noteId)
.get()
.await()

firestore.runBatch { batch ->
ratings.documents.forEach { batch.delete(it.reference) }
batch.delete(noteReference.fileDocument())
batch.delete(noteReference)
}.await()
Expand Down Expand Up @@ -193,7 +217,7 @@ class FirebaseNoteRepository(
*/
private fun observePosts(
query: Query,
defaultUploaderEmail: String = ""
defaultUploaderName: String = ""
): Flow<List<Post>> = callbackFlow {
val listener = query.addSnapshotListener { snapshot, exception ->
if (exception != null) {
Expand All @@ -202,7 +226,7 @@ class FirebaseNoteRepository(
}

val posts = snapshot?.documents
?.mapNotNull { document -> toPost(document, defaultUploaderEmail) }
?.mapNotNull { document -> toPost(document, defaultUploaderName) }
?.sortedByDescending { it.timeMills }
.orEmpty()

Expand All @@ -212,9 +236,14 @@ class FirebaseNoteRepository(
awaitClose { listener.remove() }
}

/**
* Migration'dan önce yazılmış notlar hâlâ tam `uploaderEmail` taşıyor; okunurken
* adı ondan türetiyoruz. Yeni notlarda alan hiç yok — bkz.
* `firestore-tests/strip-uploader-email.js`.
*/
private fun toPost(
document: DocumentSnapshot,
defaultUploaderEmail: String = ""
defaultUploaderName: String = ""
): Post? {
val title = document.getString("title") ?: return null
val createdAt = try {
Expand All @@ -229,13 +258,12 @@ class FirebaseNoteRepository(
id = document.id,
title = title,
desc = document.getString("description") ?: "",
authorEmail = document.getString("uploaderEmail") ?: defaultUploaderEmail,
uploaderName = document.displayName() ?: defaultUploaderName,
department = document.getString("department") ?: "",
timeMills = createdAt,
uploaderUid = document.getString("uploaderUid") ?: "",
avgRating = document.getDouble("avgRating")
?: document.getLong("avgRating")?.toDouble()
?: 0.0,
// Eski notlar hâlâ bir `avgRating` alanı taşıyor; okunmuyor. Ortalama
// artık ne saklanıyor ne de gösteriliyor — bkz. firestore.rules.
ratingCount = document.getLong("ratingCount")
?: document.getDouble("ratingCount")?.toLong()
?: 0L,
Expand All @@ -248,6 +276,11 @@ class FirebaseNoteRepository(
)
}

/** Yeni şekil önce; eski notlarda ad e-postadan türetiliyor. */
private fun DocumentSnapshot.displayName(): String? =
getString("uploaderName")
?: getString("uploaderEmail")?.uploaderName()

private fun DocumentReference.fileDocument(): DocumentReference =
collection(CONTENT_COLLECTION).document(FILE_DOCUMENT)

Expand All @@ -257,7 +290,7 @@ class FirebaseNoteRepository(
return LeaderboardEntry(
docId = document.id,
title = title,
uploaderEmail = document.getString("uploaderEmail") ?: "",
uploaderName = document.displayName() ?: "",
department = document.getString("department") ?: "",
ratingCount = document.getLong("ratingCount") ?: 0L,
ratingSum = document.getLong("ratingSum") ?: 0L
Expand All @@ -266,6 +299,8 @@ class FirebaseNoteRepository(

private companion object {
const val NOTES_COLLECTION = "notes"
const val RATINGS_COLLECTION = "ratings"
const val NOTE_ID_FIELD = "noteId"
const val CONTENT_COLLECTION = "content"
const val FILE_DOCUMENT = "file"
const val FILE_DATA_FIELD = "fileData"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ class FirebaseRatingRepository(
noteReference,
mapOf(
RATING_SUM_FIELD to totals.sum,
RATING_COUNT_FIELD to totals.count,
AVERAGE_RATING_FIELD to totals.average
RATING_COUNT_FIELD to totals.count
)
)
transaction.set(
Expand All @@ -71,7 +70,6 @@ class FirebaseRatingRepository(
)

RatingResult(
average = totals.average,
count = totals.count,
sum = totals.sum,
updatedExistingRating = totals.updatedExistingRating
Expand All @@ -85,7 +83,6 @@ class FirebaseRatingRepository(
const val UPLOADER_UID_FIELD = "uploaderUid"
const val RATING_SUM_FIELD = "ratingSum"
const val RATING_COUNT_FIELD = "ratingCount"
const val AVERAGE_RATING_FIELD = "avgRating"
const val RATING_FIELD = "rating"
const val USER_ID_FIELD = "uid"
const val NOTE_ID_FIELD = "noteId"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface NoteRepository {
suspend fun createNote(
draft: NoteDraft,
uploaderUid: String,
uploaderEmail: String,
uploaderName: String,
department: String
)

Expand All @@ -28,7 +28,7 @@ interface NoteRepository {

fun observeNotesByUploader(
uploaderUid: String,
defaultUploaderEmail: String
defaultUploaderName: String
): Flow<List<Post>>

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class PostAdapter(
tvDesc.text = post.desc
tvUploader.text = post.uploaderName
tvDept.text = post.department.take(3).uppercase()
tvRating.text = String.format("%.1f", post.avgRating)
tvRating.text = post.ratingSum.toString()
tvRatingCount.text = root.context.getString(R.string.vote_count, post.ratingCount)

root.setOnClickListener {
Expand Down Expand Up @@ -88,7 +88,7 @@ class PostAdapter(
oldItem.id == newItem.id

// Post bir data class; `==` bütün alanları karşılaştırıyor, dolayısıyla
// nota yeni bir oy geldiğinde avgRating farkı buradan yakalanıyor.
// nota yeni bir oy geldiğinde ratingSum farkı buradan yakalanıyor.
override fun areContentsTheSame(oldItem: Post, newItem: Post) =
oldItem == newItem
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class NoteDetailFragment : Fragment() {
tvTag.text = post.tag.uppercase()
}

tvAvgRating.text = String.format("%.1f", post.avgRating)
tvRatingSum.text = post.ratingSum.toString()
tvRatingCount.text = getString(R.string.rating_count, post.ratingCount)
}

Expand Down
Loading
Loading